Merge remote-tracking branch 'origin/dev' into api-groupbyUser

# Conflicts:
#	api/schemas.py
This commit is contained in:
Taha Yassine Kraiem 2022-02-02 12:30:09 +01:00
commit b88c43aa08
80 changed files with 11736 additions and 13407 deletions

View file

@ -28,7 +28,8 @@ jwt_algorithm=HS512
jwt_exp_delta_seconds=2592000
jwt_issuer=openreplay-default-foss
jwt_secret="SET A RANDOM STRING HERE"
peers=http://utilities-openreplay.app.svc.cluster.local:9000/assist/%s/peers
Opeers=http://utilities-openreplay.app.svc.cluster.local:9000/assist/%s/peers
peers=http://utilities-openreplay.app.svc.cluster.local:9000/assist/%s/sockets-list
pg_dbname=postgres
pg_host=postgresql.db.svc.cluster.local
pg_password=asayerPostgres

View file

@ -53,11 +53,11 @@ def add_edit(tenant_id, project_id, data):
else:
return add(tenant_id=tenant_id,
project_id=project_id,
host=data["host"], api_key=data["apiKeyId"], api_key_id=data["apiKey"], indexes=data["indexes"],
host=data["host"], api_key=data["apiKey"], api_key_id=data["apiKeyId"], indexes=data["indexes"],
port=data["port"])
def __get_es_client(host, port, api_key_id, api_key, use_ssl=False, timeout=29):
def __get_es_client(host, port, api_key_id, api_key, use_ssl=False, timeout=15):
host = host.replace("http://", "").replace("https://", "")
try:
args = {

View file

@ -99,10 +99,21 @@ def comment_assignment(projectId: int, sessionId: int, issueId: str, data: schem
@app.get('/{projectId}/events/search', tags=["events"])
def events_search(projectId: int, q: str, type: Union[schemas.FilterType, schemas.EventType] = None, key: str = None,
def events_search(projectId: int, q: str,
type: Union[schemas.FilterType, schemas.EventType, schemas.PerformanceEventType] = None,
key: str = None,
source: str = None, context: schemas.CurrentContext = Depends(OR_context)):
if len(q) == 0:
return {"data": []}
if isinstance(type, schemas.PerformanceEventType) \
and type in [schemas.PerformanceEventType.location_dom_complete,
schemas.PerformanceEventType.location_largest_contentful_paint_time,
schemas.PerformanceEventType.location_ttfb,
schemas.PerformanceEventType.location_avg_cpu_load,
schemas.PerformanceEventType.location_avg_memory_usage
]:
type = schemas.EventType.location
result = events.search_pg2(text=q, event_type=type, project_id=projectId, source=source, key=key)
return result

View file

@ -479,14 +479,16 @@ class _SessionSearchEventRaw(__MixedSearchFilter):
if values.get("type") == PerformanceEventType.fetch_failed:
return values
assert values.get("source") is not None, "source should not be null for PerformanceEventType"
assert values.get("sourceOperator") is not None \
, "sourceOperator should not be null for PerformanceEventType"
assert isinstance(values["source"], list) and len(values["source"]) > 0, \
"source should not be empty for PerformanceEventType"
assert values.get("sourceOperator") is not None, \
"sourceOperator should not be null for PerformanceEventType"
if values["type"] == PerformanceEventType.time_between_events:
assert len(values.get("value", [])) == 2, \
f"must provide 2 Events as value for {PerformanceEventType.time_between_events}"
assert isinstance(values["value"][0], _SessionSearchEventRaw) \
and isinstance(values["value"][1], _SessionSearchEventRaw) \
, f"event should be of type _SessionSearchEventRaw for {PerformanceEventType.time_between_events}"
and isinstance(values["value"][1], _SessionSearchEventRaw), \
f"event should be of type _SessionSearchEventRaw for {PerformanceEventType.time_between_events}"
else:
for c in values["source"]:
assert isinstance(c, int), f"source value should be of type int for {values.get('type')}"
@ -578,8 +580,14 @@ class FunnelSearchPayloadSchema(FlatSessionsSearchPayloadSchema):
range_value: Optional[str] = Field(None)
sort: Optional[str] = Field(None)
order: Optional[str] = Field(None)
events_order: Optional[SearchEventOrder] = Field(default=SearchEventOrder._then, const=True)
group_by_user: Optional[bool] = Field(default=False, const=True)
@root_validator(pre=True)
def enforce_default_values(cls, values):
values["eventsOrder"] = SearchEventOrder._then
return values
class Config:
alias_generator = attribute_to_camel_case
@ -690,4 +698,4 @@ class UpdateCustomMetricsSchema(CreateCustomMetricsSchema):
class SavedSearchSchema(FunnelSchema):
pass
filter: FlatSessionsSearchPayloadSchema = Field([])

View file

@ -1,11 +1,11 @@
package cache
import (
import (
. "openreplay/backend/pkg/messages"
// . "openreplay/backend/pkg/db/types"
// . "openreplay/backend/pkg/db/types"
)
func (c *PGCache) insertSessionEnd(sessionID uint64, timestamp uint64 ) error {
func (c *PGCache) insertSessionEnd(sessionID uint64, timestamp uint64) error {
//duration, err := c.Conn.InsertSessionEnd(sessionID, timestamp)
_, err := c.Conn.InsertSessionEnd(sessionID, timestamp)
if err != nil {
@ -20,7 +20,6 @@ func (c *PGCache) insertSessionEnd(sessionID uint64, timestamp uint64 ) error {
return nil
}
func (c *PGCache) InsertIssueEvent(sessionID uint64, crash *IssueEvent) error {
session, err := c.GetSession(sessionID)
if err != nil {
@ -29,7 +28,6 @@ func (c *PGCache) InsertIssueEvent(sessionID uint64, crash *IssueEvent) error {
return c.Conn.InsertIssueEvent(sessionID, session.ProjectID, crash)
}
func (c *PGCache) InsertUserID(sessionID uint64, userID *IOSUserID) error {
if err := c.Conn.InsertIOSUserID(sessionID, userID); err != nil {
return err
@ -38,7 +36,7 @@ func (c *PGCache) InsertUserID(sessionID uint64, userID *IOSUserID) error {
if err != nil {
return err
}
session.UserID = userID.Value
session.UserID = &userID.Value
return nil
}
@ -69,11 +67,9 @@ func (c *PGCache) InsertMetadata(sessionID uint64, metadata *Metadata) error {
if keyNo == 0 {
// insert project metadata
}
if err := c.Conn.InsertMetadata(sessionID, keyNo, metadata.Value); err != nil {
return err
}
session.SetMetadata(keyNo, metadata.Value)
return nil
}

View file

@ -1,42 +1,41 @@
package cache
import (
import (
"errors"
. "openreplay/backend/pkg/messages"
. "openreplay/backend/pkg/db/types"
. "openreplay/backend/pkg/messages"
)
func (c *PGCache) InsertWebSessionStart(sessionID uint64, s *SessionStart) error {
if c.sessions[ sessionID ] != nil {
if c.sessions[sessionID] != nil {
return errors.New("This session already in cache!")
}
c.sessions[ sessionID ] = &Session{
SessionID: sessionID,
Platform: "web",
Timestamp: s.Timestamp,
ProjectID: uint32(s.ProjectID),
c.sessions[sessionID] = &Session{
SessionID: sessionID,
Platform: "web",
Timestamp: s.Timestamp,
ProjectID: uint32(s.ProjectID),
TrackerVersion: s.TrackerVersion,
RevID: s.RevID,
UserUUID: s.UserUUID,
UserOS: s.UserOS,
UserOSVersion: s.UserOSVersion,
UserDevice: s.UserDevice,
UserCountry: s.UserCountry,
RevID: s.RevID,
UserUUID: s.UserUUID,
UserOS: s.UserOS,
UserOSVersion: s.UserOSVersion,
UserDevice: s.UserDevice,
UserCountry: s.UserCountry,
// web properties (TODO: unite different platform types)
UserAgent: s.UserAgent,
UserBrowser: s.UserBrowser,
UserBrowserVersion: s.UserBrowserVersion,
UserDeviceType: s.UserDeviceType,
UserAgent: s.UserAgent,
UserBrowser: s.UserBrowser,
UserBrowserVersion: s.UserBrowserVersion,
UserDeviceType: s.UserDeviceType,
UserDeviceMemorySize: s.UserDeviceMemorySize,
UserDeviceHeapSize: s.UserDeviceHeapSize,
UserID: s.UserID,
UserDeviceHeapSize: s.UserDeviceHeapSize,
UserID: &s.UserID,
}
if err := c.Conn.InsertSessionStart(sessionID, c.sessions[ sessionID ]); err != nil {
c.sessions[ sessionID ] = nil
if err := c.Conn.InsertSessionStart(sessionID, c.sessions[sessionID]); err != nil {
c.sessions[sessionID] = nil
return err
}
return nil;
return nil
}
func (c *PGCache) InsertWebSessionEnd(sessionID uint64, e *SessionEnd) error {
@ -54,4 +53,3 @@ func (c *PGCache) InsertWebErrorEvent(sessionID uint64, e *ErrorEvent) error {
session.ErrorsCount += 1
return nil
}

View file

@ -1,11 +1,11 @@
package postgres
import (
"math"
"math"
"openreplay/backend/pkg/hashid"
"openreplay/backend/pkg/url"
. "openreplay/backend/pkg/messages"
"openreplay/backend/pkg/url"
)
// TODO: change messages and replace everywhere to e.Index
@ -172,11 +172,12 @@ func (conn *Conn) InsertWebErrorEvent(sessionID uint64, projectID uint32, e *Err
}
defer tx.rollback()
errorID := hashid.WebErrorID(projectID, e)
if err = tx.exec(`
INSERT INTO errors
(error_id, project_id, source, name, message, payload)
VALUES
($1, $2, $3, $4, $5, $6)
($1, $2, $3, $4, $5, $6::jsonb)
ON CONFLICT DO NOTHING`,
errorID, projectID, e.Source, e.Name, e.Message, e.Payload,
); err != nil {

View file

@ -1,11 +1,12 @@
package postgres
//import . "openreplay/backend/pkg/messages"
import . "openreplay/backend/pkg/db/types"
import . "openreplay/backend/pkg/db/types"
//import "log"
func (conn *Conn) GetSession(sessionID uint64) (*Session, error) {
s := &Session{ SessionID: sessionID }
s := &Session{SessionID: sessionID}
var revID, userOSVersion *string
if err := conn.queryRow(`
SELECT platform,
@ -21,13 +22,13 @@ func (conn *Conn) GetSession(sessionID uint64) (*Session, error) {
`,
sessionID,
).Scan(&s.Platform,
&s.Duration, &s.ProjectID, &s.Timestamp,
&s.UserUUID, &s.UserOS, &userOSVersion,
&s.UserDevice, &s.UserDeviceType, &s.UserCountry,
&revID, &s.TrackerVersion,
&s.UserID, &s.UserAnonymousID,
&s.Metadata1, &s.Metadata2, &s.Metadata3, &s.Metadata4, &s.Metadata5,
&s.Metadata6, &s.Metadata7, &s.Metadata8, &s.Metadata9, &s.Metadata10); err != nil {
&s.Duration, &s.ProjectID, &s.Timestamp,
&s.UserUUID, &s.UserOS, &userOSVersion,
&s.UserDevice, &s.UserDeviceType, &s.UserCountry,
&revID, &s.TrackerVersion,
&s.UserID, &s.UserAnonymousID,
&s.Metadata1, &s.Metadata2, &s.Metadata3, &s.Metadata4, &s.Metadata5,
&s.Metadata6, &s.Metadata7, &s.Metadata8, &s.Metadata9, &s.Metadata10); err != nil {
return nil, err
}
if userOSVersion != nil { // TODO: choose format, make f
@ -35,7 +36,7 @@ func (conn *Conn) GetSession(sessionID uint64) (*Session, error) {
}
if revID != nil {
s.RevID = *revID
}
}
return s, nil
}
@ -103,4 +104,4 @@ func (conn *Conn) GetSession(sessionID uint64) (*Session, error) {
// }
// }
// return list
// }
// }

View file

@ -1,46 +1,47 @@
package types
type Session struct {
SessionID uint64
Timestamp uint64
ProjectID uint32
SessionID uint64
Timestamp uint64
ProjectID uint32
TrackerVersion string
RevID string
UserUUID string
UserOS string
UserOSVersion string
UserDevice string
UserCountry string
RevID string
UserUUID string
UserOS string
UserOSVersion string
UserDevice string
UserCountry string
Duration *uint64
PagesCount int
EventsCount int
ErrorsCount int
UserID string // pointer??
Duration *uint64
PagesCount int
EventsCount int
ErrorsCount int
UserID *string // pointer??
UserAnonymousID *string
Metadata1 *string
Metadata2 *string
Metadata3 *string
Metadata4 *string
Metadata5 *string
Metadata6 *string
Metadata7 *string
Metadata8 *string
Metadata9 *string
Metadata10 *string
Metadata1 *string
Metadata2 *string
Metadata3 *string
Metadata4 *string
Metadata5 *string
Metadata6 *string
Metadata7 *string
Metadata8 *string
Metadata9 *string
Metadata10 *string
Platform string
// Only-web properties
UserAgent string
UserBrowser string
UserBrowserVersion string
UserDeviceType string
UserAgent string
UserBrowser string
UserBrowserVersion string
UserDeviceType string
UserDeviceMemorySize uint64
UserDeviceHeapSize uint64
UserDeviceHeapSize uint64
}
func (s *Session) SetMetadata(keyNo uint, value string) {
switch (keyNo) {
switch keyNo {
case 1:
s.Metadata1 = &value
case 2:
@ -62,4 +63,4 @@ func (s *Session) SetMetadata(keyNo uint, value string) {
case 10:
s.Metadata10 = &value
}
}
}

View file

@ -2,7 +2,7 @@ package intervals
const EVENTS_COMMIT_INTERVAL = 30 * 1000
const HEARTBEAT_INTERVAL = 2 * 60 * 1000
const INTEGRATIONS_REQUEST_INTERVAL = 2 * 60 * 1000
const INTEGRATIONS_REQUEST_INTERVAL = 1 * 60 * 1000
const EVENTS_PAGE_EVENT_TIMEOUT = 2 * 60 * 1000
const EVENTS_INPUT_EVENT_TIMEOUT = 2 * 60 * 1000
const EVENTS_PERFORMANCE_AGGREGATION_TIMEOUT = 2 * 60 * 1000

View file

@ -8,14 +8,14 @@ import (
"os/signal"
"syscall"
"openreplay/backend/pkg/db/cache"
"openreplay/backend/pkg/db/postgres"
"openreplay/backend/pkg/env"
"openreplay/backend/pkg/messages"
"openreplay/backend/pkg/queue"
"openreplay/backend/pkg/queue/types"
"openreplay/backend/pkg/messages"
"openreplay/backend/pkg/db/postgres"
"openreplay/backend/pkg/db/cache"
"openreplay/backend/services/db/heuristics"
)
)
var pg *cache.PGCache
@ -23,62 +23,62 @@ func main() {
log.SetFlags(log.LstdFlags | log.LUTC | log.Llongfile)
initStats()
pg = cache.NewPGCache(postgres.NewConn(env.String("POSTGRES_STRING")), 1000 * 60 * 20)
pg = cache.NewPGCache(postgres.NewConn(env.String("POSTGRES_STRING")), 1000*60*20)
defer pg.Close()
heurFinder := heuristics.NewHandler()
consumer := queue.NewMessageConsumer(
env.String("GROUP_DB"),
[]string{
[]string{
env.String("TOPIC_RAW_IOS"),
env.String("TOPIC_TRIGGER"),
},
func(sessionID uint64, msg messages.Message, _ *types.Meta) {
if err := insertMessage(sessionID, msg); err != nil {
if !postgres.IsPkeyViolation(err) {
log.Printf("Message Insertion Error %v, SessionID: %v, Message: %v", err,sessionID, msg)
}
return
}
},
func(sessionID uint64, msg messages.Message, _ *types.Meta) {
if err := insertMessage(sessionID, msg); err != nil {
if !postgres.IsPkeyViolation(err) {
log.Printf("Message Insertion Error %v, SessionID: %v, Message: %v", err, sessionID, msg)
}
return
}
session, err := pg.GetSession(sessionID)
session, err := pg.GetSession(sessionID)
if err != nil {
// Might happen due to the assets-related message TODO: log only if session is necessary for this kind of message
log.Printf("Error on session retrieving from cache: %v, SessionID: %v, Message: %v", err, sessionID, msg)
return;
return
}
err = insertStats(session, msg)
if err != nil {
log.Printf("Stats Insertion Error %v; Session: %v, Message: %v", err, session, msg)
}
err = insertStats(session, msg)
if err != nil {
log.Printf("Stats Insertion Error %v; Session: %v, Message: %v", err, session, msg)
}
heurFinder.HandleMessage(session, msg)
heurFinder.IterateSessionReadyMessages(sessionID, func(msg messages.Message) {
// TODO: DRY code (carefully with the return statement logic)
if err := insertMessage(sessionID, msg); err != nil {
if !postgres.IsPkeyViolation(err) {
log.Printf("Message Insertion Error %v; Session: %v, Message %v", err, session, msg)
}
return
}
if !postgres.IsPkeyViolation(err) {
log.Printf("Message Insertion Error %v; Session: %v, Message %v", err, session, msg)
}
return
}
err = insertStats(session, msg)
if err != nil {
log.Printf("Stats Insertion Error %v; Session: %v, Message %v", err, session, msg)
}
err = insertStats(session, msg)
if err != nil {
log.Printf("Stats Insertion Error %v; Session: %v, Message %v", err, session, msg)
}
})
},
)
consumer.DisableAutoCommit()
sigchan := make(chan os.Signal, 1)
signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM)
signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM)
tick := time.Tick(15 * time.Second)
tick := time.Tick(15 * time.Second)
log.Printf("Db service started\n")
log.Printf("Db service started\n")
for {
select {
case sig := <-sigchan:
@ -88,11 +88,11 @@ func main() {
case <-tick:
if err := commitStats(); err != nil {
log.Printf("Error on stats commit: %v", err)
}
}
// TODO?: separate stats & regular messages
if err := consumer.Commit(); err != nil {
log.Printf("Error on consumer commit: %v", err)
}
}
default:
err := consumer.ConsumeNext()
if err != nil {
@ -101,4 +101,4 @@ func main() {
}
}
}
}

View file

@ -1,12 +1,13 @@
package integration
import (
"sync"
"fmt"
"encoding/json"
"fmt"
"log"
"sync"
"openreplay/backend/pkg/messages"
"openreplay/backend/pkg/db/postgres"
"openreplay/backend/pkg/messages"
"openreplay/backend/pkg/utime"
)
@ -135,6 +136,8 @@ func (c *client) Request() {
c.requestData.LastAttemptTimestamp = utime.CurrentTimestamp()
err := c.requester.Request(c)
if err != nil {
log.Println("ERRROR L139")
log.Println(err)
c.handleError(err)
c.requestData.UnsuccessfullAttemptsCount++;
} else {

View file

@ -1,193 +1,222 @@
package integration
import (
elasticlib "github.com/elastic/go-elasticsearch/v7"
"bytes"
"context"
"time"
b64 "encoding/base64"
"encoding/json"
"fmt"
"bytes"
elasticlib "github.com/elastic/go-elasticsearch/v7"
"log"
"strconv"
"time"
"openreplay/backend/pkg/utime"
"openreplay/backend/pkg/messages"
"openreplay/backend/pkg/utime"
)
type elasticsearch struct {
Host string
Port json.Number
ApiKeyId string //`json:"api_key_id"`
ApiKey string //`json:"api_key"`
Indexes string
Host string
Port json.Number
ApiKeyId string //`json:"api_key_id"`
ApiKey string //`json:"api_key"`
Indexes string
}
type elasticsearchLog struct {
Message string
Time time.Time `json:"utc_time"` // Should be parsed automatically from RFC3339
Time time.Time `json:"utc_time"` // Should be parsed automatically from RFC3339
}
type elasticResponce struct {
Hits struct {
//Total struct {
// Value int
//}
Hits []struct {
Id string `json:"_id"`
Source json.RawMessage `json:"_source"`
}
}
ScrollId string `json:"_scroll_id"`
}
func (es *elasticsearch) Request(c* client) error {
func (es *elasticsearch) Request(c *client) error {
address := es.Host + ":" + es.Port.String()
apiKey := b64.StdEncoding.EncodeToString([]byte(es.ApiKeyId + ":" + es.ApiKey))
cfg := elasticlib.Config{
Addresses: []string{
address,
},
Username: es.ApiKeyId,
Password: es.ApiKey,
Addresses: []string{
address,
},
//Username: es.ApiKeyId,
//Password: es.ApiKey,
APIKey: apiKey,
}
esC, err := elasticlib.NewClient(cfg)
if err != nil {
log.Println("Error while creating new ES client")
log.Println(err)
return err
}
// TODO: ping/versions/ client host check
// res0, err := esC.Info()
// if err != nil {
// log.Printf("ELASTIC Error getting info: %s", err)
// }
// defer res0.Body.Close()
// // Check response status
// if res0.IsError() {
// log.Printf("ELASTIC Error: %s", res0.String())
// }
// log.Printf("ELASTIC Info: %v ", res0.String())
gteTs := c.getLastMessageTimestamp() + 1000 // Sec or millisec to add ?
log.Printf("gteTs: %v ", gteTs)
var buf bytes.Buffer
query := map[string]interface{}{
"query": map[string]interface{}{
"bool": map[string]interface{}{
"filter": []map[string]interface{}{
map[string]interface{}{
"match": map[string]interface{} {
"message": map[string]interface{}{
"query": "openReplaySessionToken=", // asayer_session_id=
},
},
},
map[string]interface{}{
"range": map[string]interface{} {
"utc_time": map[string]interface{}{
"gte": strconv.FormatUint(gteTs, 10),
"lte": "now",
},
},
},
map[string]interface{}{
"term": map[string]interface{}{
"tags": "error",
},
},
},
},
},
}
if err := json.NewEncoder(&buf).Encode(query); err != nil {
return fmt.Errorf("Error encoding the query: %s", err)
}
query := map[string]interface{}{
"query": map[string]interface{}{
"bool": map[string]interface{}{
"filter": []map[string]interface{}{
map[string]interface{}{
"match": map[string]interface{}{
"message": map[string]interface{}{
"query": "openReplaySessionToken=", // asayer_session_id=
},
},
},
map[string]interface{}{
"range": map[string]interface{}{
"utc_time": map[string]interface{}{
"gte": strconv.FormatUint(gteTs, 10),
"lte": "now",
},
},
},
map[string]interface{}{
"term": map[string]interface{}{
"tags": "error",
},
},
},
},
},
}
if err := json.NewEncoder(&buf).Encode(query); err != nil {
return fmt.Errorf("Error encoding the query: %s", err)
}
res, err := esC.Search(
esC.Search.WithContext(context.Background()),
esC.Search.WithIndex(es.Indexes),
esC.Search.WithSize(1000),
esC.Search.WithScroll(time.Minute * 2),
esC.Search.WithBody(&buf),
esC.Search.WithSort("timestamp:asc"),
)
if err != nil {
return fmt.Errorf("Error getting response: %s", err)
}
defer res.Body.Close()
if res.IsError() {
var e map[string]interface{}
if err := json.NewDecoder(res.Body).Decode(&e); err != nil {
return fmt.Errorf("Error parsing the response body: %v", err)
} else {
return fmt.Errorf("Elasticsearch [%s] %s: %s",
res.Status(),
e["error"],//.(map[string]interface{})["type"],
e["error"],//.(map[string]interface{})["reason"],
)
}
}
esC.Search.WithContext(context.Background()),
esC.Search.WithIndex(es.Indexes),
esC.Search.WithSize(1000),
esC.Search.WithScroll(time.Minute*2),
esC.Search.WithBody(&buf),
esC.Search.WithSort("utc_time:asc"),
)
if err != nil {
return fmt.Errorf("Error getting response: %s", err)
}
defer res.Body.Close()
if res.IsError() {
var e map[string]interface{}
if err := json.NewDecoder(res.Body).Decode(&e); err != nil {
log.Printf("Error parsing the Error response body: %v\n", err)
return fmt.Errorf("Error parsing the Error response body: %v", err)
} else {
log.Printf("Elasticsearch Error [%s] %s: %s\n",
res.Status(),
e["error"],
e["error"],
)
return fmt.Errorf("Elasticsearch Error [%s] %s: %s",
res.Status(),
e["error"],
e["error"],
)
}
}
for {
var esResp elasticResponce
if err := json.NewDecoder(res.Body).Decode(&esResp); err != nil {
return fmt.Errorf("Error parsing the response body: %s", err)
}
if len(esResp.Hits.Hits) == 0 {
break
}
for {
var esResp map[string]interface{}
if err := json.NewDecoder(res.Body).Decode(&esResp); err != nil {
return fmt.Errorf("Error parsing the response body: %s", err)
// If no error, then convert response to a map[string]interface
}
for _, hit := range esResp.Hits.Hits {
var esLog elasticsearchLog
if err = json.Unmarshal(hit.Source, &esLog); err != nil {
c.errChan <- err
if _, ok := esResp["hits"]; !ok {
log.Printf("Hits not found in \n%v\n", esResp)
break
}
hits := esResp["hits"].(map[string]interface{})["hits"].([]interface{})
if len(hits) == 0 {
log.Println("No hits found")
break
}
log.Printf("received %d hits", len(hits))
for _, hit := range hits {
// Parse the attributes/fields of the document
doc := hit.(map[string]interface{})
source := doc["_source"].(map[string]interface{})
if _, ok := source["message"]; !ok {
log.Printf("message not found in doc \n%v\n", doc)
c.errChan <- fmt.Errorf("message not found in doc '%v' ", doc)
continue
}
if _, ok := source["utc_time"]; !ok {
log.Printf("utc_time not found in doc \n%v\n", doc)
c.errChan <- fmt.Errorf("utc_time not found in doc '%v' ", doc)
continue
}
parsedTime, err := time.Parse(time.RFC3339, source["utc_time"].(string))
if err != nil {
log.Println("cannot parse time")
c.errChan <- fmt.Errorf("cannot parse RFC3339 time of doc '%v' ", doc)
continue
}
esLog := elasticsearchLog{Message: source["message"].(string), Time: parsedTime}
docID := doc["_id"]
token, err := GetToken(esLog.Message)
if err != nil {
log.Printf("Error generating token: %s\n", err)
c.errChan <- err
continue
}
//parsedTime, err := time.Parse(time.RFC3339, esLog.Timestamp)
//if err != nil {
// c.errChan <- err
// continue
//}
timestamp := uint64(utime.ToMilliseconds(esLog.Time))
c.setLastMessageTimestamp(timestamp)
var sessionID uint64
sessionID, err = strconv.ParseUint(token, 10, 64)
if err != nil {
log.Printf("Error converting token to uint46: %s\n", err)
sessionID = 0
}
payload, err := json.Marshal(source)
if err != nil {
log.Printf("Error converting source to json: %v\n", source)
continue
}
c.evChan <- &SessionErrorEvent{
//SessionID: sessionID,
Token: token,
SessionID: sessionID,
Token: token,
RawErrorEvent: &messages.RawErrorEvent{
Source: "elasticsearch",
Source: "elasticsearch",
Timestamp: timestamp,
Name: hit.Id, // sure?
Payload: string(hit.Source),
Name: fmt.Sprintf("%v", docID),
Payload: string(payload),
},
}
}
res, err = esC.Scroll(
esC.Scroll.WithContext(context.Background()),
esC.Scroll.WithScrollID(esResp.ScrollId),
esC.Scroll.WithScroll(time.Minute * 2),
)
if err != nil {
return fmt.Errorf("Error getting scroll response: %s", err)
}
defer res.Body.Close()
if res.IsError() {
var e map[string]interface{}
if err := json.NewDecoder(res.Body).Decode(&e); err != nil {
return fmt.Errorf("Error parsing the response body: %v", err)
} else {
return fmt.Errorf("Elasticsearch [%s] %s: %s",
res.Status(),
e["error"],//.(map[string]interface{})["type"],
e["error"],//.(map[string]interface{})["reason"],
)
}
}
}
if _, ok := esResp["_scroll_id"]; !ok {
log.Println("_scroll_id not found")
break
}
log.Println("Scrolling...")
scrollId := esResp["_scroll_id"]
res, err = esC.Scroll(
esC.Scroll.WithContext(context.Background()),
esC.Scroll.WithScrollID(fmt.Sprintf("%v", scrollId)),
esC.Scroll.WithScroll(time.Minute*2),
)
if err != nil {
return fmt.Errorf("Error getting scroll response: %s", err)
}
defer res.Body.Close()
if res.IsError() {
var e map[string]interface{}
if err := json.NewDecoder(res.Body).Decode(&e); err != nil {
return fmt.Errorf("Error parsing the response body: %v", err)
} else {
return fmt.Errorf("Elasticsearch [%s] %s: %s",
res.Status(),
e["error"], //.(map[string]interface{})["type"],
e["error"], //.(map[string]interface{})["reason"],
)
}
}
}
return nil
}
}

View file

@ -8,11 +8,11 @@ import (
"os/signal"
"syscall"
"openreplay/backend/pkg/db/postgres"
"openreplay/backend/pkg/env"
"openreplay/backend/pkg/intervals"
"openreplay/backend/pkg/messages"
"openreplay/backend/pkg/queue"
"openreplay/backend/pkg/db/postgres"
"openreplay/backend/pkg/token"
"openreplay/backend/services/integrations/clientManager"
)
@ -42,7 +42,7 @@ func main() {
}
})
producer:= queue.NewProducer()
producer := queue.NewProducer()
defer producer.Close(15000)
listener, err := postgres.NewIntegrationsListener(POSTGRES_STRING)
@ -66,10 +66,10 @@ func main() {
pg.Close()
os.Exit(0)
case <-tick:
// log.Printf("Requesting all...\n")
log.Printf("Requesting all...\n")
manager.RequestAll()
case event := <-manager.Events:
// log.Printf("New integration event: %v\n", *event.RawErrorEvent)
log.Printf("New integration event: %+v\n", *event.RawErrorEvent)
sessionID := event.SessionID
if sessionID == 0 {
sessData, err := tokenizer.Parse(event.Token)
@ -89,7 +89,7 @@ func main() {
log.Printf("Postgres Update request_data error: %v\n", err)
}
case err := <-listener.Errors:
log.Printf("Postgres listen error: %v\n", err)
log.Printf("Postgres listen error: %v\n", err)
case iPointer := <-listener.Integrations:
log.Printf("Integration update: %v\n", *iPointer)
err := manager.Update(iPointer)

View file

@ -37,7 +37,8 @@ jwt_algorithm=HS512
jwt_exp_delta_seconds=2592000
jwt_issuer=openreplay-default-ee
jwt_secret="SET A RANDOM STRING HERE"
peers=http://utilities-openreplay.app.svc.cluster.local:9000/assist/%s/peers
Opeers=http://utilities-openreplay.app.svc.cluster.local:9000/assist/%s/peers
peers=http://utilities-openreplay.app.svc.cluster.local:9000/assist/%s/sockets-list
pg_dbname=postgres
pg_host=postgresql.db.svc.cluster.local
pg_password=asayerPostgres

View file

@ -0,0 +1,9 @@
BEGIN;
CREATE OR REPLACE FUNCTION openreplay_version()
RETURNS text AS
$$
SELECT 'v1.5.0-ee'
$$ LANGUAGE sql IMMUTABLE;
ALTER TYPE public.error_source ADD VALUE IF NOT EXISTS 'elasticsearch';
COMMIT;

View file

@ -7,7 +7,7 @@ CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE OR REPLACE FUNCTION openreplay_version()
RETURNS text AS
$$
SELECT 'v1.4.0-ee'
SELECT 'v1.5.0-ee'
$$ LANGUAGE sql IMMUTABLE;
@ -504,7 +504,7 @@ $$
IF NOT EXISTS(SELECT *
FROM pg_type typ
WHERE typ.typname = 'error_source') THEN
CREATE TYPE error_source AS ENUM ('js_exception','bugsnag','cloudwatch','datadog','newrelic','rollbar','sentry','stackdriver','sumologic');
CREATE TYPE error_source AS ENUM ('js_exception','bugsnag','cloudwatch','datadog','newrelic','rollbar','sentry','stackdriver','sumologic', 'elasticsearch');
END IF;
IF NOT EXISTS(SELECT *

View file

@ -1,5 +1,6 @@
import cn from 'classnames';
import { connect } from 'react-redux';
import { List } from 'immutable';
import withPageTitle from 'HOCs/withPageTitle';
import {
fetchFavoriteList as fetchFavoriteSessionList
@ -30,6 +31,8 @@ import SessionSearchField from 'Shared/SessionSearchField'
import SavedSearch from 'Shared/SavedSearch'
import LiveSessionList from './LiveSessionList'
import SessionSearch from 'Shared/SessionSearch';
import { clearSearch } from 'Duck/search';
import { Button } from 'UI';
const weakEqual = (val1, val2) => {
if (!!val1 === false && !!val2 === false) return true;
@ -78,7 +81,8 @@ const allowedQueryKeys = [
fetchFunnelsList,
resetFunnel,
resetFunnelFilters,
setFunnelPage
setFunnelPage,
clearSearch,
})
@withPageTitle("Sessions - OpenReplay")
export default class BugFinder extends React.PureComponent {
@ -174,8 +178,13 @@ export default class BugFinder extends React.PureComponent {
className="mb-5"
>
<div className="flex items-center">
<div style={{ width: "70%", marginRight: "10px"}}><SessionSearchField /></div>
<SavedSearch />
<div style={{ width: "65%", marginRight: "10px"}}><SessionSearchField /></div>
<div className="flex items-center" style={{ width: "35%"}}>
<SavedSearch />
<Button plain className="ml-auto" onClick={() => this.props.clearSearch()}>
<span className="font-medium">Clear</span>
</Button>
</div>
</div>
<SessionSearch />
{/* <EventFilter /> */}

View file

@ -5,6 +5,7 @@
display: flex;
align-items: center;
background-color: white;
width: 100%;
& input {
height: 24px;
font-size: 13px !important;
@ -12,6 +13,7 @@
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
border: solid thin transparent !important;
width: 100%;
}
& .right {

View file

@ -3,6 +3,7 @@ import FilterOperator from '../FilterOperator';
import FilterSelection from '../FilterSelection';
import FilterValue from '../FilterValue';
import { Icon } from 'UI';
import FilterSource from '../FilterSource';
interface Props {
filterIndex: number;
@ -21,16 +22,40 @@ function FitlerItem(props: Props) {
const onOperatorChange = (e, { name, value }) => {
props.onUpdate({ ...filter, operator: value })
}
const onSourceOperatorChange = (e, { name, value }) => {
props.onUpdate({ ...filter, sourceOperator: value })
}
return (
<div className="flex items-center hover:bg-active-blue -mx-5 px-5 py-2">
<div className="flex items-start w-full">
{ !isFilter && <div className="mt-1 flex-shrink-0 border w-6 h-6 text-xs flex justify-center rounded-full bg-gray-light-shade mr-2">{filterIndex+1}</div> }
<FilterSelection filter={filter} onFilterClick={replaceFilter} />
<FilterOperator filter={filter} onChange={onOperatorChange} className="mx-2 flex-shrink-0"/>
{/* Filter with Source */}
{ filter.hasSource && (
<>
<FilterOperator
options={filter.sourceOperatorOptions}
onChange={onSourceOperatorChange}
className="mx-2 flex-shrink-0"
value={filter.sourceOperator}
/>
<FilterSource filter={filter} onUpdate={props.onUpdate} />
</>
)}
{/* Filter values */}
<FilterOperator
options={filter.operatorOptions}
onChange={onOperatorChange}
className="mx-2 flex-shrink-0"
value={filter.operator}
/>
<FilterValue filter={filter} onUpdate={props.onUpdate} />
</div>
<div className="flex self-start mt-2 ml-auto">
<div className="flex flex-shrink-0 self-start mt-1 ml-auto px-2">
<div
className="cursor-pointer p-1"
onClick={props.onRemoveFilter}

View file

@ -1,6 +1,6 @@
import React, { useState} from 'react';
import FilterItem from '../FilterItem';
import { SegmentSelection } from 'UI';
import { SegmentSelection, Popup } from 'UI';
interface Props {
// filters: any[]; // event/filter
@ -14,6 +14,7 @@ function FilterList(props: Props) {
const filters = filter.filters;
const hasEvents = filter.filters.filter(i => i.isEvent).size > 0;
const hasFilters = filter.filters.filter(i => !i.isEvent).size > 0;
let rowIndex = 0;
const onRemoveFilter = (filterIndex) => {
const newFilters = filters.filter((_filter, i) => {
@ -28,9 +29,17 @@ function FilterList(props: Props) {
{ hasEvents && (
<>
<div className="flex items-center mb-2">
<div className="mb-2 text-sm color-gray-medium mr-auto">EVENTS</div>
<div className="text-sm color-gray-medium mr-auto">EVENTS</div>
<div className="flex items-center">
<div className="mr-2 color-gray-medium text-sm">Events Order</div>
<div className="mr-2 color-gray-medium text-sm" style={{ textDecoration: 'underline dotted'}}>
<Popup
trigger={<div>Events Order</div>}
content={ `Events Order` }
size="tiny"
inverted
position="top center"
/>
</div>
<SegmentSelection
primary
name="eventsOrder"
@ -50,7 +59,7 @@ function FilterList(props: Props) {
</div>
{filters.map((filter, filterIndex) => filter.isEvent ? (
<FilterItem
filterIndex={filterIndex}
filterIndex={rowIndex++}
filter={filter}
onUpdate={(filter) => props.onUpdateFilter(filterIndex, filter)}
onRemoveFilter={() => onRemoveFilter(filterIndex) }
@ -62,7 +71,7 @@ function FilterList(props: Props) {
{hasFilters && (
<>
{hasEvents && <div className='border-t -mx-5 mb-2' />}
{hasEvents && <div className='border-t -mx-5 mb-4' />}
<div className="mb-2 text-sm color-gray-medium mr-auto">FILTERS</div>
{filters.map((filter, filterIndex) => !filter.isEvent ? (
<FilterItem

View file

@ -0,0 +1,17 @@
.wrapper {
border-radius: 3px;
border: solid thin $gray-light;
padding: 20px;
}
.optionItem {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
&:hover {
background-color: $active-blue;
color: $teal !important;
& svg {
fill: $teal !important;
}
}
}

View file

@ -1,6 +1,8 @@
import React from 'react';
import { Icon } from 'UI';
import { connect } from 'react-redux';
import cn from 'classnames';
import stl from './FilterModal.css';
interface Props {
filters: any,
@ -9,14 +11,14 @@ interface Props {
function FilterModal(props: Props) {
const { filters, onFilterClick = () => null } = props;
return (
<div className="border p-3" style={{ width: '490px', height: '400px', overflowY: 'auto'}}>
<div className={stl.wrapper} style={{ width: '490px', height: '400px', overflowY: 'auto'}}>
<div className="" style={{ columns: "100px 2" }}>
{filters && Object.keys(filters).map((key) => (
<div className="p-3 aspect-w-1">
<div className="uppercase font-medium mb-1">{key}</div>
<div className="mb-6">
<div className="uppercase font-medium mb-1 color-gray-medium tracking-widest text-sm">{key}</div>
<div>
{filters[key].map((filter: any) => (
<div className="flex items-center py-2 cursor-pointer hover:bg-gray-lightest -mx-2 px-2" onClick={() => onFilterClick(filter)}>
<div className={cn(stl.optionItem, "flex items-center py-2 cursor-pointer -mx-2 px-2")} onClick={() => onFilterClick(filter)}>
<Icon name={filter.icon} size="16"/>
<span className="ml-2">{filter.label}</span>
</div>

View file

@ -4,21 +4,21 @@ import { Dropdown, Icon } from 'UI';
import stl from './FilterOperator.css';
interface Props {
filter: any; // event/filter
// filter: any; // event/filter
onChange: (e, { name, value }) => void;
className?: string;
options?: any;
value?: string;
}
function FilterOperator(props: Props) {
const { filter, onChange, className = '' } = props;
console.log('FilterOperator', filter.operator);
const { options, value, onChange, className = '' } = props;
return (
<Dropdown
className={ cn(stl.operatorDropdown, className, 'hover:bg-gray-light-shade') }
options={ filter.operatorOptions }
options={ options }
name="operator"
value={ filter.operator }
value={ value }
onChange={ onChange }
placeholder="Select operator"
icon={ <Icon className="ml-5" name="chevron-down" size="12" /> }

View file

@ -0,0 +1,10 @@
.inputField {
display: inline-block;
margin-right: 10px;
border: solid thin $gray-light;
border-radius: 3px;
height: 26px;
background-color: $white;
padding: 0 5px;
max-width: 100px;
}

View file

@ -0,0 +1,40 @@
import { FilterType } from 'App/types/filter/filterType';
import React from 'react';
import stl from './FilterSource.css';
interface Props {
filter: any,
onUpdate: (filter) => void;
}
function FilterSource(props: Props) {
const { filter } = props;
console.log('FilterSource', filter.source);
const onChange = ({ target: { value, name } }) => {
props.onUpdate({ ...filter, [name]: [value] })
}
const renderFiled = () => {
switch(filter.sourceType) {
case FilterType.NUMBER:
return (
<input
name="source"
className={stl.inputField}
value={filter.source[0]}
onBlur={onChange}
type="number"
/>
)
}
}
return (
<div>
{ renderFiled()}
</div>
);
}
export default FilterSource;

View file

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

View file

@ -22,7 +22,7 @@ function FilterValue(props: Props) {
props.onUpdate({ ...filter, value: newValues })
}
const onSelect = (e, item, valueIndex) => {
const onChange = (e, item, valueIndex) => {
const newValues = filter.value.map((_, _index) => {
if (_index === valueIndex) {
return item.value;
@ -61,7 +61,7 @@ function FilterValue(props: Props) {
value={value}
filter={filter}
options={filter.options}
onChange={(e, { name, value }) => onSelect(e, { value }, valueIndex)}
onChange={(e, { name, value }) => onChange(e, { value }, valueIndex)}
/>
)
case FilterType.ISSUE:
@ -72,7 +72,7 @@ function FilterValue(props: Props) {
value={value}
filter={filter}
options={filter.options}
onChange={(e, { name, value }) => onSelect(e, { value }, valueIndex)}
onChange={(e, { name, value }) => onChange(e, { value }, valueIndex)}
onAddValue={onAddValue}
onRemoveValue={() => onRemoveValue(valueIndex)}
showCloseButton={showCloseButton}
@ -96,7 +96,7 @@ function FilterValue(props: Props) {
type="number"
name={`${filter.key}-${valueIndex}`}
value={value}
onChange={(e) => onSelect(e, { value: e.target.value }, valueIndex)}
onChange={(e) => onChange(e, { value: e.target.value }, valueIndex)}
/>
)
case FilterType.MULTIPLE:
@ -112,7 +112,7 @@ function FilterValue(props: Props) {
params={{ type: filter.key }}
headerText={''}
// placeholder={''}
onSelect={(e, item) => onSelect(e, item, valueIndex)}
onSelect={(e, item) => onChange(e, item, valueIndex)}
/>
)
}

View file

@ -4,10 +4,10 @@
background-color: white !important;
display: flex;
align-items: center;
height: 30px;
height: 26px;
& .right {
height: 28px;
height: 24px;
display: flex;
align-items: stretch;
padding: 0;

View file

@ -18,11 +18,11 @@ interface Props {
onAddValue?: () => void;
}
function FilterValueDropdown(props: Props) {
const { multiple = false, search = false, options, onChange, value, className = '', showCloseButton = true, showOrButton = true } = props;
const { filter, multiple = false, search = false, options, onChange, value, className = '', showCloseButton = true, showOrButton = true } = props;
// const options = []
return (
<div className={stl.wrapper}>
<div className={stl.wrapper}>
<Dropdown
search={search}
className={ cn(stl.operatorDropdown, className) }

View file

@ -1,18 +1,25 @@
import React, { useState } from 'react';
import { connect } from 'react-redux';
import { save } from 'Duck/filters';
import { Button } from 'UI';
import { IconButton } from 'UI';
import SaveSearchModal from 'Shared/SaveSearchModal'
interface Props {
filter: any;
savedSearch: any;
}
function SaveFilterButton(props) {
function SaveFilterButton(props: Props) {
const { savedSearch } = props;
const [showModal, setshowModal] = useState(false)
return (
<div>
<Button onClick={() => setshowModal(true)}>SAVE FILTER</Button>
{ savedSearch ? (
<IconButton className="mr-2" onClick={() => setshowModal(true)} primaryText label="UPDATE SEARCH" icon="zoom-in" />
) : (
<IconButton className="mr-2" onClick={() => setshowModal(true)} primaryText label="SAVE SEARCH" icon="zoom-in" />
)}
<SaveSearchModal
show={showModal}
closeHandler={() => setshowModal(false)}
@ -22,5 +29,6 @@ function SaveFilterButton(props) {
}
export default connect(state => ({
filter: state.getIn([ 'filters', 'appliedFilter' ]),
filter: state.getIn([ 'search', 'instance' ]),
savedSearch: state.getIn([ 'search', 'savedSearch' ]),
}), { save })(SaveFilterButton);

View file

@ -1,15 +1,23 @@
@import 'mixins.css';
.modalHeader {
display: flex !important;
align-items: center;
justify-content: space-between;
display: flex !important;
align-items: center;
justify-content: space-between;
}
.cancelButton {
@mixin plainButton;
background-color: transparent !important;
border: solid thin transparent !important;
color: $teal !important;
&:hover {
background-color: $active-blue !important;
}
}
.applyButton {
@mixin basicButton;
background-color: white !important;
border: solid thin $active-blue-border !important;
color: $teal !important;
&:hover {
background-color: $active-blue !important;
}
}

View file

@ -1,33 +1,51 @@
import React from 'react';
import React, { useState } from 'react';
import { connect } from 'react-redux';
import { edit, save } from 'Duck/search';
import { edit, save, remove } from 'Duck/search';
import { Button, Modal, Form, Icon, Checkbox } from 'UI';
import { confirm } from 'UI/Confirmation';
import stl from './SaveSearchModal.css';
interface Props {
filter: any;
loading: boolean;
edit: (filter: any) => void;
save: (filter: any) => Promise<void>;
save: (searchId, name, filter: any) => Promise<void>;
show: boolean;
closeHandler: () => void;
savedSearch: any;
remove: (filterId: number) => Promise<void>;
}
function SaveSearchModal(props: Props) {
const { filter, loading, show, closeHandler } = props;
const [name, setName] = useState(props.savedSearch ? props.savedSearch.name : '');
const { savedSearch, filter, loading, show, closeHandler } = props;
const onNameChange = ({ target: { value } }) => {
props.edit({ name: value });
// props.edit({ name: value });
setName(value);
};
const onSave = () => {
const { filter, closeHandler } = props;
if (filter.name.trim() === '') return;
props.save(filter).then(function() {
if (name.trim() === '') return;
props.save(savedSearch ? savedSearch.searchId : null, name, filter).then(function() {
// this.props.fetchFunnelsList();
closeHandler();
});
}
const onDelete = async () => {
if (await confirm({
header: 'Confirm',
confirmButton: 'Yes, Delete',
confirmation: `Are you sure you want to permanently delete this alert?`
})) {
props.remove(savedSearch.searchId).then(() => {
closeHandler();
});
}
}
return (
<Modal size="tiny" open={ show }>
<Modal.Header className={ stl.modalHeader }>
@ -50,29 +68,33 @@ function SaveSearchModal(props: Props) {
autoFocus={ true }
// className={ stl.name }
name="name"
value={ filter.name }
value={ name }
onChange={ onNameChange }
placeholder="Title"
/>
</Form.Field>
</Form>
</Modal.Content>
<Modal.Actions className="">
<Button
primary
onClick={ onSave }
loading={ loading }
>
{ filter.exists() ? 'Modify' : 'Save' }
</Button>
<Button className={ stl.cancelButton } marginRight onClick={ closeHandler }>{ 'Cancel' }</Button>
<Modal.Actions className="flex items-center">
<div className="mr-auto">
<Button
primary
onClick={ onSave }
loading={ loading }
>
{ savedSearch ? 'Update' : 'Create' }
</Button>
<Button className={ stl.cancelButton } marginRight onClick={ closeHandler }>{ 'Cancel' }</Button>
</div>
{ savedSearch && <Button className={ stl.cancelButton } marginRight onClick={ onDelete }>{ 'Delete' }</Button> }
</Modal.Actions>
</Modal>
);
}
export default connect(state => ({
savedSearch: state.getIn([ 'search', 'savedSearch' ]),
filter: state.getIn(['search', 'instance']),
loading: state.getIn([ 'filters', 'saveRequest', 'loading' ]) ||
state.getIn([ 'filters', 'updateRequest', 'loading' ]),
}), { edit, save })(SaveSearchModal);
loading: state.getIn([ 'search', 'saveRequest', 'loading' ]) ||
state.getIn([ 'search', 'updateRequest', 'loading' ]),
}), { edit, save, remove })(SaveSearchModal);

View file

@ -0,0 +1,4 @@
.disabled {
opacity: 0.5 !important;
pointer-events: none;
}

View file

@ -2,14 +2,20 @@ import React, { useState, useEffect } from 'react';
import { Button, Icon } from 'UI';
import SavedSearchDropdown from './components/SavedSearchDropdown';
import { connect } from 'react-redux';
import { fetchList as fetchListSavedSearch } from 'Duck/filters';
import { fetchList as fetchListSavedSearch } from 'Duck/search';
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
import cn from 'classnames';
import { list } from 'App/components/BugFinder/CustomFilters/filterModal.css';
import stl from './SavedSearch.css';
interface Props {
fetchListSavedSearch: () => void;
list: any;
savedSearch: any;
}
function SavedSearch(props) {
const { list } = props;
const { savedSearch } = props;
const [showMenu, setShowMenu] = useState(false)
useEffect(() => {
@ -22,19 +28,29 @@ function SavedSearch(props) {
onClickOutside={() => setShowMenu(false)}
>
<div className="relative">
<Button prime outline size="small"
className="flex items-center"
onClick={() => setShowMenu(true)}
>
<span className="mr-2">Search Saved</span>
<Icon name="ellipsis-v" color="teal" size="14" />
</Button>
<div className={cn("flex items-center", { [stl.disabled] : list.size === 0})}>
<Button prime outline size="small"
className="flex items-center"
onClick={() => setShowMenu(true)}
>
<span className="mr-2">Search Saved</span>
<Icon name="ellipsis-v" color="teal" size="14" />
</Button>
{ savedSearch && (
<div className="flex items-center ml-2">
<Icon name="search" size="14" />
<span className="color-gray-medium px-1">Viewing:</span>
<span className="font-medium">{savedSearch.name}</span>
</div>
)}
</div>
{ showMenu && (
<div
className="absolute right-0 bg-white border rounded z-50"
className="absolute left-0 bg-white border rounded z-50"
style={{ top: '33px', width: '200px' }}
>
<SavedSearchDropdown list={props.list}/>
<SavedSearchDropdown list={props.list} onClose={() => setShowMenu(false)} />
</div>
)}
</div>
@ -43,5 +59,6 @@ function SavedSearch(props) {
}
export default connect(state => ({
list: state.getIn([ 'filters', 'list' ]),
list: state.getIn([ 'search', 'list' ]),
savedSearch: state.getIn([ 'search', 'savedSearch' ])
}), { fetchListSavedSearch })(SavedSearch);

View file

@ -4,4 +4,12 @@
z-index: 999;
display: flex;
flex-direction: column;
max-height: 250px;
overflow-y: auto;
}
.rowItem {
&:hover {
color: $teal;
}
}

View file

@ -1,24 +1,70 @@
import React from 'react';
import stl from './SavedSearchDropdown.css';
import cn from 'classnames';
import { Icon } from 'UI';
import { applySavedSearch, remove, edit } from 'Duck/search'
import { connect } from 'react-redux';
import { confirm } from 'UI/Confirmation';
interface Props {
list: Array<any>
applySavedSearch: (filter: any) => void
remove: (id: string) => Promise<void>
onClose: () => void,
edit: (filter: any) => void,
}
function Row ({ name }) {
function Row ({ name, onClick, onClickEdit, onDelete }) {
return (
<div className="p-2 cursor-pointer hover:bg-gray-lightest">{name}</div>
<div
onClick={onClick}
className={cn(stl.rowItem, "flex items-center cursor-pointer hover:bg-active-blue")}
>
<div className="px-3 py-2">{name}</div>
<div className="ml-auto flex items-center">
<div className="cursor-pointer px-2 hover:bg-active-blue" onClick={onClickEdit}><Icon name="pencil" size="14" /></div>
{/* <div className="cursor-pointer px-2 hover:bg-active-blue" onClick={onDelete}><Icon name="trash" size="14" /></div> */}
</div>
</div>
)
}
function SavedSearchDropdown(props: Props) {
const onClick = (item) => {
props.applySavedSearch(item)
props.edit(item.filter)
props.onClose()
}
const onDelete = async (instance) => {
if (await confirm({
header: 'Confirm',
confirmButton: 'Yes, Delete',
confirmation: `Are you sure you want to permanently delete this search?`
})) {
props.remove(instance.alertId).then(() => {
// toggleForm(null, false);
});
}
}
const onClickEdit = (instance) => {
// toggleForm(instance);
}
return (
<div className={stl.wrapper}>
{props.list.map(item => (
<Row key={item.searchId} name={item.name} />
<Row
key={item.searchId}
name={item.name}
onClick={() => onClick(item)}
onDelete={() => onDelete(item) }
onClickEdit={() => onClickEdit(item)}
/>
))}
</div>
);
}
export default SavedSearchDropdown;
export default connect(null, { applySavedSearch, remove, edit })(SavedSearchDropdown);

View file

@ -1,4 +1,5 @@
import React from 'react';
import { List } from 'immutable';
import FilterList from 'Shared/Filters/FilterList';
import FilterSelection from 'Shared/Filters/FilterSelection';
import SaveFilterButton from 'Shared/SaveFilterButton';
@ -12,6 +13,8 @@ interface Props {
}
function SessionSearch(props) {
const { appliedFilter } = props;
const hasEvents = appliedFilter.filters.filter(i => i.isEvent).size > 0;
const hasFilters = appliedFilter.filters.filter(i => !i.isEvent).size > 0;
const onAddFilter = (filter) => {
filter.value = [""]
@ -55,16 +58,14 @@ function SessionSearch(props) {
const clearSearch = () => {
props.edit({
filters: [],
filters: List(),
});
}
return (
return (hasEvents || hasFilters) ? (
<div className="border bg-white rounded mt-4">
<div className="p-5">
<FilterList
// filters={appliedFilter.filter.filters.toJS()}
filter={appliedFilter}
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
@ -83,16 +84,13 @@ function SessionSearch(props) {
</div>
<div className="ml-auto flex items-center">
<SaveFilterButton />
<Button onClick={clearSearch}>CLEAR STEPS</Button>
<Button plain>SAVE FUNNEL</Button>
<IconButton primaryText label="SAVE FUNNEL" icon="filter" />
</div>
</div>
</div>
);
) : <></>;
}
export default connect(state => ({
appliedFilter: state.getIn([ 'search', 'instance' ]),
}), { edit })(SessionSearch);
// appliedFilter: state.getIn([ 'filters', 'appliedFilter' ]),
}), { edit })(SessionSearch);

View file

@ -2,9 +2,11 @@ import React, { useRef, useState } from 'react';
import { connect } from 'react-redux';
import stl from './SessionSearchField.css';
import { Input } from 'UI';
import FilterModal from 'Shared/EventFilter/FilterModal';
import { fetchList as fetchEventList } from 'Duck/events';
import FilterModal from 'Shared/Filters/FilterModal';
// import { fetchList as fetchFilterSearch } from 'Duck/events';
import { fetchFilterSearch } from 'Duck/search';
import { debounce } from 'App/utils';
import { edit as editFilter } from 'Duck/search';
import {
addEvent, applyFilter, moveEvent, clearEvents,
addCustomFilter, addAttribute, setSearchQuery, setActiveFlow, setFilterOption
@ -12,16 +14,28 @@ import {
interface Props {
setSearchQuery: (query: string) => void;
fetchEventList: (query: any) => void;
searchQuery: string
fetchFilterSearch: (query: any) => void;
searchQuery: string;
appliedFilter: any;
editFilter: typeof editFilter;
}
function SessionSearchField(props: Props) {
const debounceFetchEventList = debounce(props.fetchEventList, 1000)
const { appliedFilter } = props;
const debounceFetchFilterSearch = debounce(props.fetchFilterSearch, 1000)
const [showModal, setShowModal] = useState(false)
const onSearchChange = (e, { value }) => {
// props.setSearchQuery(value)
debounceFetchEventList({ q: value });
debounceFetchFilterSearch({ q: value });
}
const onAddFilter = (filter) => {
filter.value = [""]
const newFilters = appliedFilter.filters.concat(filter);
props.editFilter({
...appliedFilter.filter,
filters: newFilters,
});
}
return (
@ -30,7 +44,7 @@ function SessionSearchField(props: Props) {
inputProps={ { "data-openreplay-label": "Search", "autocomplete": "off" } }
className={stl.searchField}
onFocus={ () => setShowModal(true) }
onBlur={ () => setTimeout(setShowModal, 100, false) }
onBlur={ () => setTimeout(setShowModal, 50, false) }
// ref={ this.inputRef }
onChange={ onSearchChange }
// onKeyUp={this.onKeyUp}
@ -44,21 +58,28 @@ function SessionSearchField(props: Props) {
autocomplete="off"
/>
<FilterModal
{/* <FilterModal
close={ () => setShowModal(false) }
displayed={ showModal }
// displayed={ true }
// loading={ loading }
// searchedEvents={ searchedEvents }
searchQuery={ props.searchQuery }
/>
/> */}
{ showModal && (
<div className="absolute left-0 top-20 border shadow rounded bg-white z-50">
<FilterModal
onFilterClick={onAddFilter}
/>
</div>
)}
</div>
);
}
export default connect(state => ({
events: state.getIn([ 'filters', 'appliedFilter', 'events' ]),
appliedFilter: state.getIn([ 'filters', 'appliedFilter' ]),
// appliedFilter: state.getIn([ 'filters', 'appliedFilter' ]),
searchQuery: state.getIn([ 'filters', 'searchQuery' ]),
appliedFilterKeys: state.getIn([ 'filters', 'appliedFilter', 'filters' ])
.map(({type}) => type).toJS(),
@ -66,4 +87,5 @@ export default connect(state => ({
loading: state.getIn([ 'events', 'loading' ]),
strict: state.getIn([ 'filters', 'appliedFilter', 'strict' ]),
blink: state.getIn([ 'funnels', 'blink' ]),
}), { setSearchQuery, fetchEventList })(SessionSearchField);
appliedFilter: state.getIn(['search', 'instance']),
}), { setSearchQuery, fetchFilterSearch, editFilter })(SessionSearchField);

View file

@ -0,0 +1,143 @@
export const options = [
{
key: 'is',
text: 'is',
value: 'is'
}, {
key: 'isNot',
text: 'is not',
value: 'isNot'
}, {
key: 'startsWith',
text: 'starts with',
value: 'startsWith'
}, {
key: 'endsWith',
text: 'ends with',
value: 'endsWith'
}, {
key: 'contains',
text: 'contains',
value: 'contains'
}, {
key: 'notContains',
text: 'not contains',
value: 'notContains'
}, {
key: 'hasAnyValue',
text: 'has any value',
value: 'hasAnyValue'
}, {
key: 'hasNoValue',
text: 'has no value',
value: 'hasNoValue'
},
{
key: 'isSignedUp',
text: 'is signed up',
value: 'isSignedUp'
}, {
key: 'notSignedUp',
text: 'not signed up',
value: 'notSignedUp'
},
{
key: 'before',
text: 'before',
value: 'before'
}, {
key: 'after',
text: 'after',
value: 'after'
}, {
key: 'on',
text: 'on',
value: 'on'
}, {
key: 'notOn',
text: 'not on',
value: 'notOn'
}, {
key: 'inRage',
text: 'in rage',
value: 'inRage'
}, {
key: 'notInRage',
text: 'not in rage',
value: 'notInRage'
}, {
key: 'withinLast',
text: 'within last',
value: 'withinLast'
}, {
key: 'notWithinLast',
text: 'not within last',
value: 'notWithinLast'
},
{
key: 'greaterThan',
text: 'greater than',
value: 'greaterThan'
}, {
key: 'lessThan',
text: 'less than',
value: 'lessThan'
}, {
key: 'equal',
text: 'equal',
value: 'equal'
}, {
key: 'not equal',
text: 'not equal',
value: 'not equal'
},
{
key: 'onSelector',
text: 'on selector',
value: 'onSelector'
}, {
key: 'onText',
text: 'on text',
value: 'onText'
}, {
key: 'onComponent',
text: 'on component',
value: 'onComponent'
},
{
key: 'onAny',
text: 'on any',
value: 'onAny'
}
];
const filterKeys = ['is', 'isNot'];
const stringFilterKeys = ['is', 'isNot', 'contains', 'startsWith', 'endsWith', 'notContains'];
const targetFilterKeys = ['on', 'notOn', 'onAny'];
const signUpStatusFilterKeys = ['isSignedUp', 'notSignedUp'];
const rangeFilterKeys = ['before', 'after', 'on', 'inRange', 'notInRange', 'withInLast', 'notWithInLast'];
export const baseOperators = options.filter(({key}) => filterKeys.includes(key));
export const stringOperators = options.filter(({key}) => stringFilterKeys.includes(key));
export const targetOperators = options.filter(({key}) => targetFilterKeys.includes(key));
export const booleanOperators = [
{ key: 'true', text: 'true', value: 'true' },
{ key: 'false', text: 'false', value: 'false' },
]
export const customOperators = [
{ key: '=', text: '=', value: '=' },
{ key: '<', text: '<', value: '<' },
{ key: '>', text: '>', value: '>' },
{ key: '<=', text: '<=', value: '<=' },
{ key: '>=', text: '>=', value: '>=' },
]
export default {
options,
baseOperators,
stringOperators,
targetOperators,
booleanOperators,
customOperators,
}

View file

@ -19,3 +19,4 @@ export {
SLACK as CHANNEL_SLACK,
WEBHOOK as CHANNEL_WEBHOOK
} from './schedule';
export { default } from './filterOptions';

View file

@ -16,7 +16,6 @@ import NewFilter, { filtersMap } from 'Types/filter/newFilter';
const filterOptions = {}
Object.keys(filtersMap).forEach(key => {
// const filter = NewFilter(filtersMap[key]);
const filter = filtersMap[key];
if (filterOptions.hasOwnProperty(filter.category)) {
filterOptions[filter.category].push(filter);
@ -24,8 +23,6 @@ Object.keys(filtersMap).forEach(key => {
filterOptions[filter.category] = [filter];
}
})
console.log('filterOptions', filterOptions)
// for (var i = 0; i < newFiltersList.length; i++) {

View file

@ -5,6 +5,7 @@ import { createFetch, fetchListType, fetchType, saveType, removeType, editType,
import { createRequestReducer, ROOT_KEY } from './funcTools/request';
import { array, request, success, failure, createListUpdater, mergeReducers } from './funcTools/tools';
import Filter from 'Types/filter';
import NewFilter from 'Types/filter/newFilter';
import SavedFilter from 'Types/filter/savedFilter';
import { errors as errorsRoute, isRoute } from "App/routes";
import { fetchList as fetchSessionList } from './sessions';
@ -13,13 +14,16 @@ import { fetchList as fetchErrorsList } from './errors';
const ERRORS_ROUTE = errorsRoute();
const name = "search";
const idKey = "metricId";
const idKey = "searchId";
const FETCH_LIST = fetchListType(name);
const FETCH_FILTER_SEARCH = fetchListType(`${name}/FILTER_SEARCH`);
const FETCH = fetchType(name);
const SAVE = saveType(name);
const EDIT = editType(name);
const REMOVE = removeType(name);
const APPLY_SAVED_SEARCH = `${name}/APPLY_SAVED_SEARCH`;
const CLEAR_SEARCH = `${name}/CLEAR_SEARCH`;
const UPDATE = `${name}/UPDATE`;
const APPLY = `${name}/APPLY`;
const SET_ALERT_METRIC_ID = `${name}/SET_ALERT_METRIC_ID`;
@ -28,16 +32,18 @@ function chartWrapper(chart = []) {
return chart.map(point => ({ ...point, count: Math.max(point.count, 0) }));
}
// const updateItemInList = createListUpdater(idKey);
// const updateInstance = (state, instance) => state.getIn([ "instance", idKey ]) === instance[ idKey ]
// ? state.mergeIn([ "instance" ], instance)
// : state;
const savedSearchIdKey = 'searchId'
const updateItemInList = createListUpdater(savedSearchIdKey);
const updateInstance = (state, instance) => state.getIn([ "savedSearch", savedSearchIdKey ]) === instance[savedSearchIdKey]
? state.mergeIn([ "savedSearch" ], instance)
: state;
const initialState = Map({
list: List(),
alertMetricId: null,
instance: new Filter({ filters: [] }),
savedFilter: new SavedFilter({ filters: [] }),
savedSearch: null,
filterSearchList: List(),
});
// Metric - Series - [] - filters
@ -53,14 +59,19 @@ function reducer(state = initialState, action = {}) {
)
: state.mergeIn(['instance'], action.filter);
case success(SAVE):
return state.mergeIn([ 'instance' ], action.data);
return updateItemInList(updateInstance(state, action.data), action.data);
// return state.mergeIn([ 'instance' ], action.data);
case success(REMOVE):
return state.update('list', list => list.filter(item => item.metricId !== action.id));
return state.update('list', list => list.filter(item => item.searchId !== action.id));
case success(FETCH):
return state.set("instance", ErrorInfo(action.data));
case success(FETCH_LIST):
const { data } = action;
return state.set("list", List(data.map(CustomMetric)));
return state.set("list", List(data.map(SavedFilter)));
case success(FETCH_FILTER_SEARCH):
return state.set("filterSearchList", action.data.map(NewFilter));
case APPLY_SAVED_SEARCH:
return state.set('savedSearch', action.filter);
}
return state;
}
@ -73,13 +84,14 @@ export default mergeReducers(
}),
);
const filterMap = ({value, type, key, operator, source, custom, isEvent }) => ({
// value: Array.isArray(value) ? value: [value],
const filterMap = ({value, key, operator, sourceOperator, source, custom, isEvent }) => ({
value: value.filter(i => i !== '' && i !== null),
custom,
type: key,
key, operator,
// key,
operator,
source,
sourceOperator,
isEvent
});
@ -100,7 +112,7 @@ export const edit = reduceThenFetchResource((instance) => ({
instance,
}));
export const remove = createRemove(name);
export const remove = createRemove(name, (id) => `/saved_search/${id}`);
export const applyFilter = reduceThenFetchResource((filter, fromUrl=false) => ({
type: APPLY,
@ -108,6 +120,16 @@ export const applyFilter = reduceThenFetchResource((filter, fromUrl=false) => ({
fromUrl,
}));
export const applySavedSearch = (filter) => (dispatch, getState) => {
// console.log('applySavedSearch', filter);
// export const applySavedSearch = (filter) => ({
dispatch(edit(filter ? filter.filter : new Filter({ fitlers: []})));
return dispatch({
type: APPLY_SAVED_SEARCH,
filter,
})
};
export const updateSeries = (index, series) => ({
type: UPDATE,
index,
@ -122,12 +144,13 @@ export function fetch(id) {
}
}
export function save(instance) {
export function save(id, name, instance) {
instance = instance instanceof SavedFilter ? instance : new SavedFilter(instance);
return {
types: SAVE.array,
call: client => client.post('/saved_search', {
name: instance.name,
filter: instance.filter.toSaveData(),
call: client => client.post(!id ? '/saved_search' : `/saved_search/${id}`, {
name: name,
filter: instance.toSaveData(),
}),
instance,
};
@ -145,4 +168,20 @@ export function setAlertMetricId(id) {
type: SET_ALERT_METRIC_ID,
id,
};
}
export function fetchFilterSearch(params) {
return {
types: FETCH_FILTER_SEARCH.array,
call: client => client.get('/events/search', params),
params,
};
}
export const clearSearch = () => (dispatch, getState) => {
dispatch(applySavedSearch(null));
dispatch(edit(new Filter({ filters: [] })));
return dispatch({
type: CLEAR_SEARCH,
});
}

View file

@ -0,0 +1,13 @@
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path d="M5 8C5.26522 8 5.51957 7.89464 5.70711 7.70711C5.89464 7.51957 6 7.26522 6 7C6 6.73478 5.89464 6.48043 5.70711 6.29289C5.51957 6.10536 5.26522 6 5 6C4.73478 6 4.48043 6.10536 4.29289 6.29289C4.10536 6.48043 4 6.73478 4 7C4 7.26522 4.10536 7.51957 4.29289 7.70711C4.48043 7.89464 4.73478 8 5 8ZM9 7C9 7.26522 8.89464 7.51957 8.70711 7.70711C8.51957 7.89464 8.26522 8 8 8C7.73478 8 7.48043 7.89464 7.29289 7.70711C7.10536 7.51957 7 7.26522 7 7C7 6.73478 7.10536 6.48043 7.29289 6.29289C7.48043 6.10536 7.73478 6 8 6C8.26522 6 8.51957 6.10536 8.70711 6.29289C8.89464 6.48043 9 6.73478 9 7ZM11 8C11.2652 8 11.5196 7.89464 11.7071 7.70711C11.8946 7.51957 12 7.26522 12 7C12 6.73478 11.8946 6.48043 11.7071 6.29289C11.5196 6.10536 11.2652 6 11 6C10.7348 6 10.4804 6.10536 10.2929 6.29289C10.1054 6.48043 10 6.73478 10 7C10 7.26522 10.1054 7.51957 10.2929 7.70711C10.4804 7.89464 10.7348 8 11 8V8Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.17157 3.17157C1.92172 2.42143 2.93913 2 4 2H28C29.0609 2 30.0783 2.42143 30.8284 3.17157C31.5786 3.92172 32 4.93913 32 6V26C32 27.0609 31.5786 28.0783 30.8284 28.8284C30.0783 29.5786 29.0609 30 28 30H4C2.93913 30 1.92172 29.5786 1.17157 28.8284C0.421427 28.0783 0 27.0609 0 26V6C0 4.93913 0.421427 3.92172 1.17157 3.17157ZM30 9V6C30 5.46957 29.7893 4.96086 29.4142 4.58579C29.0391 4.21071 28.5304 4 28 4H4C3.46957 4 2.96086 4.21071 2.58579 4.58579C2.21071 4.96086 2 5.46957 2 6V9V10V12V13V26C2 26.5304 2.21071 27.0391 2.58579 27.4142C2.96086 27.7893 3.46957 28 4 28H28C28.5304 28 29.0391 27.7893 29.4142 27.4142C29.7893 27.0391 30 26.5304 30 26V13V12V10V9Z" fill="black"/>
<g clip-path="url(#clip0_113_146)">
<path d="M16 11C16.1326 11 16.2598 11.0527 16.3536 11.1464C16.4473 11.2402 16.5 11.3674 16.5 11.5V13C16.5 13.1326 16.4473 13.2598 16.3536 13.3536C16.2598 13.4473 16.1326 13.5 16 13.5C15.8674 13.5 15.7402 13.4473 15.6464 13.3536C15.5527 13.2598 15.5 13.1326 15.5 13V11.5C15.5 11.3674 15.5527 11.2402 15.6464 11.1464C15.7402 11.0527 15.8674 11 16 11V11ZM11.732 12.732C11.8258 12.6383 11.9529 12.5856 12.0855 12.5856C12.2181 12.5856 12.3452 12.6383 12.439 12.732L13.354 13.646C13.4479 13.7399 13.5006 13.8672 13.5006 14C13.5006 14.1328 13.4479 14.2601 13.354 14.354C13.2601 14.4479 13.1328 14.5006 13 14.5006C12.8672 14.5006 12.7399 14.4479 12.646 14.354L11.732 13.439C11.6383 13.3452 11.5856 13.2181 11.5856 13.0855C11.5856 12.9529 11.6383 12.8258 11.732 12.732V12.732ZM10 17C10 16.8674 10.0527 16.7402 10.1464 16.6464C10.2402 16.5527 10.3674 16.5 10.5 16.5H12.086C12.2186 16.5 12.3458 16.5527 12.4396 16.6464C12.5333 16.7402 12.586 16.8674 12.586 17C12.586 17.1326 12.5333 17.2598 12.4396 17.3536C12.3458 17.4473 12.2186 17.5 12.086 17.5H10.5C10.3674 17.5 10.2402 17.4473 10.1464 17.3536C10.0527 17.2598 10 17.1326 10 17ZM19.5 17C19.5 16.8674 19.5527 16.7402 19.6464 16.6464C19.7402 16.5527 19.8674 16.5 20 16.5H21.5C21.6326 16.5 21.7598 16.5527 21.8536 16.6464C21.9473 16.7402 22 16.8674 22 17C22 17.1326 21.9473 17.2598 21.8536 17.3536C21.7598 17.4473 21.6326 17.5 21.5 17.5H20C19.8674 17.5 19.7402 17.4473 19.6464 17.3536C19.5527 17.2598 19.5 17.1326 19.5 17ZM20.254 12.754C20.1847 12.6851 20.0921 12.6448 19.9944 12.6411C19.8968 12.6374 19.8013 12.6705 19.727 12.734L15.547 16.31C15.4507 16.3924 15.3727 16.4941 15.318 16.6084C15.2633 16.7227 15.233 16.8472 15.2292 16.9739C15.2254 17.1006 15.2481 17.2267 15.2959 17.3441C15.3436 17.4615 15.4153 17.5676 15.5065 17.6557C15.5976 17.7438 15.7062 17.8118 15.8252 17.8555C15.9441 17.8992 16.0709 17.9175 16.1974 17.9094C16.3239 17.9012 16.4473 17.8667 16.5597 17.8081C16.672 17.7495 16.7709 17.6681 16.85 17.569L20.284 13.272C20.3437 13.1972 20.3738 13.103 20.3685 13.0075C20.3631 12.9119 20.3227 12.8216 20.255 12.754H20.254Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.6639 24.889C13.6278 24.7136 12.6365 24.3358 11.7464 23.7772C10.8564 23.2186 10.085 22.4902 9.47649 21.6336C8.24747 19.9034 7.75607 17.756 8.1104 15.6635C8.46473 13.5711 9.63576 11.7051 11.3659 10.4761C13.096 9.2471 15.2435 8.7557 17.3359 9.11003C19.4285 9.46436 21.2946 10.6354 22.5237 12.3657C23.7528 14.0959 24.2442 16.2435 23.8899 18.336C23.5356 20.4286 22.3645 22.2947 20.6343 23.5238C18.9041 24.7529 16.7565 25.2444 14.6639 24.89V24.889ZM9.9989 20.606C11.8226 19.5508 13.8929 18.9967 15.9999 19C18.1859 19 20.2359 19.585 22.0009 20.606C22.6388 19.5439 22.9833 18.3314 22.9993 17.0926C23.0153 15.8537 22.7022 14.6328 22.092 13.5545C21.4818 12.4762 20.5963 11.5792 19.526 10.9551C18.4557 10.331 17.2389 10.0021 15.9999 10.0021C14.7609 10.0021 13.5441 10.331 12.4738 10.9551C11.4035 11.5792 10.518 12.4762 9.90781 13.5545C9.29758 14.6328 8.98448 15.8537 9.00048 17.0926C9.01649 18.3314 9.36102 19.5439 9.9989 20.606V20.606Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_113_146">
<rect width="16" height="16" fill="white" transform="translate(8 9)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

@ -0,0 +1,5 @@
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path d="M5 8C5.26522 8 5.51957 7.89464 5.70711 7.70711C5.89464 7.51957 6 7.26522 6 7C6 6.73478 5.89464 6.48043 5.70711 6.29289C5.51957 6.10536 5.26522 6 5 6C4.73478 6 4.48043 6.10536 4.29289 6.29289C4.10536 6.48043 4 6.73478 4 7C4 7.26522 4.10536 7.51957 4.29289 7.70711C4.48043 7.89464 4.73478 8 5 8ZM9 7C9 7.26522 8.89464 7.51957 8.70711 7.70711C8.51957 7.89464 8.26522 8 8 8C7.73478 8 7.48043 7.89464 7.29289 7.70711C7.10536 7.51957 7 7.26522 7 7C7 6.73478 7.10536 6.48043 7.29289 6.29289C7.48043 6.10536 7.73478 6 8 6C8.26522 6 8.51957 6.10536 8.70711 6.29289C8.89464 6.48043 9 6.73478 9 7ZM11 8C11.2652 8 11.5196 7.89464 11.7071 7.70711C11.8946 7.51957 12 7.26522 12 7C12 6.73478 11.8946 6.48043 11.7071 6.29289C11.5196 6.10536 11.2652 6 11 6C10.7348 6 10.4804 6.10536 10.2929 6.29289C10.1054 6.48043 10 6.73478 10 7C10 7.26522 10.1054 7.51957 10.2929 7.70711C10.4804 7.89464 10.7348 8 11 8V8Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.17157 3.17157C1.92172 2.42143 2.93913 2 4 2H28C29.0609 2 30.0783 2.42143 30.8284 3.17157C31.5786 3.92172 32 4.93913 32 6V26C32 27.0609 31.5786 28.0783 30.8284 28.8284C30.0783 29.5786 29.0609 30 28 30H4C2.93913 30 1.92172 29.5786 1.17157 28.8284C0.421427 28.0783 0 27.0609 0 26V6C0 4.93913 0.421427 3.92172 1.17157 3.17157ZM30 9V6C30 5.46957 29.7893 4.96086 29.4142 4.58579C29.0391 4.21071 28.5304 4 28 4H4C3.46957 4 2.96086 4.21071 2.58579 4.58579C2.21071 4.96086 2 5.46957 2 6V9V10V12V13V26C2 26.5304 2.21071 27.0391 2.58579 27.4142C2.96086 27.7893 3.46957 28 4 28H28C28.5304 28 29.0391 27.7893 29.4142 27.4142C29.7893 27.0391 30 26.5304 30 26V13V12V10V9Z" fill="black"/>
<path d="M23 17.5C23 19.4891 22.2098 21.3968 20.8033 22.8033C19.3968 24.2098 17.4891 25 15.5 25C13.5109 25 11.6032 24.2098 10.1967 22.8033C8.79018 21.3968 8 19.4891 8 17.5C8 15.5109 8.79018 13.6032 10.1967 12.1967C11.6032 10.7902 13.5109 10 15.5 10C17.4891 10 19.3968 10.7902 20.8033 12.1967C22.2098 13.6032 23 15.5109 23 17.5V17.5ZM19.2781 14.6594C19.2112 14.5926 19.1314 14.5401 19.0437 14.5049C18.9559 14.4697 18.862 14.4525 18.7675 14.4544C18.673 14.4564 18.5798 14.4773 18.4936 14.5161C18.4073 14.5548 18.3298 14.6106 18.2656 14.68L15.0097 18.8284L13.0475 16.8653C12.9142 16.7411 12.7379 16.6735 12.5558 16.6767C12.3736 16.6799 12.1998 16.7537 12.071 16.8825C11.9422 17.0114 11.8684 17.1852 11.8651 17.3673C11.8619 17.5495 11.9295 17.7258 12.0537 17.8591L14.5344 20.3406C14.6012 20.4073 14.6808 20.4599 14.7684 20.4952C14.8559 20.5305 14.9497 20.5477 15.0441 20.546C15.1385 20.5442 15.2316 20.5235 15.3178 20.485C15.4041 20.4465 15.4816 20.391 15.5459 20.3219L19.2884 15.6437C19.416 15.5111 19.4865 15.3337 19.4848 15.1496C19.483 14.9656 19.4092 14.7896 19.2791 14.6594H19.2781Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -0,0 +1,12 @@
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path d="M5 8C5.26522 8 5.51957 7.89464 5.70711 7.70711C5.89464 7.51957 6 7.26522 6 7C6 6.73478 5.89464 6.48043 5.70711 6.29289C5.51957 6.10536 5.26522 6 5 6C4.73478 6 4.48043 6.10536 4.29289 6.29289C4.10536 6.48043 4 6.73478 4 7C4 7.26522 4.10536 7.51957 4.29289 7.70711C4.48043 7.89464 4.73478 8 5 8ZM9 7C9 7.26522 8.89464 7.51957 8.70711 7.70711C8.51957 7.89464 8.26522 8 8 8C7.73478 8 7.48043 7.89464 7.29289 7.70711C7.10536 7.51957 7 7.26522 7 7C7 6.73478 7.10536 6.48043 7.29289 6.29289C7.48043 6.10536 7.73478 6 8 6C8.26522 6 8.51957 6.10536 8.70711 6.29289C8.89464 6.48043 9 6.73478 9 7ZM11 8C11.2652 8 11.5196 7.89464 11.7071 7.70711C11.8946 7.51957 12 7.26522 12 7C12 6.73478 11.8946 6.48043 11.7071 6.29289C11.5196 6.10536 11.2652 6 11 6C10.7348 6 10.4804 6.10536 10.2929 6.29289C10.1054 6.48043 10 6.73478 10 7C10 7.26522 10.1054 7.51957 10.2929 7.70711C10.4804 7.89464 10.7348 8 11 8V8Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.17157 3.17157C1.92172 2.42143 2.93913 2 4 2H28C29.0609 2 30.0783 2.42143 30.8284 3.17157C31.5786 3.92172 32 4.93913 32 6V26C32 27.0609 31.5786 28.0783 30.8284 28.8284C30.0783 29.5786 29.0609 30 28 30H4C2.93913 30 1.92172 29.5786 1.17157 28.8284C0.421427 28.0783 0 27.0609 0 26V6C0 4.93913 0.421427 3.92172 1.17157 3.17157ZM30 9V6C30 5.46957 29.7893 4.96086 29.4142 4.58579C29.0391 4.21071 28.5304 4 28 4H4C3.46957 4 2.96086 4.21071 2.58579 4.58579C2.21071 4.96086 2 5.46957 2 6V9V10V12V13V26C2 26.5304 2.21071 27.0391 2.58579 27.4142C2.96086 27.7893 3.46957 28 4 28H28C28.5304 28 29.0391 27.7893 29.4142 27.4142C29.7893 27.0391 30 26.5304 30 26V13V12V10V9Z" fill="black"/>
<g clip-path="url(#clip0_113_165)">
<path d="M20 9H12C11.4696 9 10.9609 9.21071 10.5858 9.58579C10.2107 9.96086 10 10.4696 10 11V23C10 23.5304 10.2107 24.0391 10.5858 24.4142C10.9609 24.7893 11.4696 25 12 25H20C20.5304 25 21.0391 24.7893 21.4142 24.4142C21.7893 24.0391 22 23.5304 22 23V11C22 10.4696 21.7893 9.96086 21.4142 9.58579C21.0391 9.21071 20.5304 9 20 9V9ZM14.646 14.646C14.6925 14.5995 14.7477 14.5626 14.8084 14.5375C14.8692 14.5123 14.9343 14.4994 15 14.4994C15.0657 14.4994 15.1308 14.5123 15.1916 14.5375C15.2523 14.5626 15.3075 14.5995 15.354 14.646C15.4005 14.6925 15.4374 14.7477 15.4625 14.8084C15.4877 14.8692 15.5006 14.9343 15.5006 15C15.5006 15.0657 15.4877 15.1308 15.4625 15.1916C15.4374 15.2523 15.4005 15.3075 15.354 15.354L13.707 17L15.354 18.646C15.4479 18.7399 15.5006 18.8672 15.5006 19C15.5006 19.1328 15.4479 19.2601 15.354 19.354C15.2601 19.4479 15.1328 19.5006 15 19.5006C14.8672 19.5006 14.7399 19.4479 14.646 19.354L12.646 17.354C12.5994 17.3076 12.5625 17.2524 12.5373 17.1916C12.5121 17.1309 12.4991 17.0658 12.4991 17C12.4991 16.9342 12.5121 16.8691 12.5373 16.8084C12.5625 16.7476 12.5994 16.6924 12.646 16.646L14.646 14.646V14.646ZM17.354 14.646L19.354 16.646C19.4006 16.6924 19.4375 16.7476 19.4627 16.8084C19.4879 16.8691 19.5009 16.9342 19.5009 17C19.5009 17.0658 19.4879 17.1309 19.4627 17.1916C19.4375 17.2524 19.4006 17.3076 19.354 17.354L17.354 19.354C17.3075 19.4005 17.2523 19.4374 17.1916 19.4625C17.1308 19.4877 17.0657 19.5006 17 19.5006C16.9343 19.5006 16.8692 19.4877 16.8084 19.4625C16.7477 19.4374 16.6925 19.4005 16.646 19.354C16.5995 19.3075 16.5626 19.2523 16.5375 19.1916C16.5123 19.1308 16.4994 19.0657 16.4994 19C16.4994 18.9343 16.5123 18.8692 16.5375 18.8084C16.5626 18.7477 16.5995 18.6925 16.646 18.646L18.293 17L16.646 15.354C16.5521 15.2601 16.4994 15.1328 16.4994 15C16.4994 14.8672 16.5521 14.7399 16.646 14.646C16.7399 14.5521 16.8672 14.4994 17 14.4994C17.1328 14.4994 17.2601 14.5521 17.354 14.646V14.646Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_113_165">
<rect width="16" height="16" fill="white" transform="translate(8 9)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -0,0 +1,5 @@
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path d="M5 8C5.26522 8 5.51957 7.89464 5.70711 7.70711C5.89464 7.51957 6 7.26522 6 7C6 6.73478 5.89464 6.48043 5.70711 6.29289C5.51957 6.10536 5.26522 6 5 6C4.73478 6 4.48043 6.10536 4.29289 6.29289C4.10536 6.48043 4 6.73478 4 7C4 7.26522 4.10536 7.51957 4.29289 7.70711C4.48043 7.89464 4.73478 8 5 8ZM9 7C9 7.26522 8.89464 7.51957 8.70711 7.70711C8.51957 7.89464 8.26522 8 8 8C7.73478 8 7.48043 7.89464 7.29289 7.70711C7.10536 7.51957 7 7.26522 7 7C7 6.73478 7.10536 6.48043 7.29289 6.29289C7.48043 6.10536 7.73478 6 8 6C8.26522 6 8.51957 6.10536 8.70711 6.29289C8.89464 6.48043 9 6.73478 9 7ZM11 8C11.2652 8 11.5196 7.89464 11.7071 7.70711C11.8946 7.51957 12 7.26522 12 7C12 6.73478 11.8946 6.48043 11.7071 6.29289C11.5196 6.10536 11.2652 6 11 6C10.7348 6 10.4804 6.10536 10.2929 6.29289C10.1054 6.48043 10 6.73478 10 7C10 7.26522 10.1054 7.51957 10.2929 7.70711C10.4804 7.89464 10.7348 8 11 8V8Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.17157 3.17157C1.92172 2.42143 2.93913 2 4 2H28C29.0609 2 30.0783 2.42143 30.8284 3.17157C31.5786 3.92172 32 4.93913 32 6V26C32 27.0609 31.5786 28.0783 30.8284 28.8284C30.0783 29.5786 29.0609 30 28 30H4C2.93913 30 1.92172 29.5786 1.17157 28.8284C0.421427 28.0783 0 27.0609 0 26V6C0 4.93913 0.421427 3.92172 1.17157 3.17157ZM30 9V6C30 5.46957 29.7893 4.96086 29.4142 4.58579C29.0391 4.21071 28.5304 4 28 4H4C3.46957 4 2.96086 4.21071 2.58579 4.58579C2.21071 4.96086 2 5.46957 2 6V9V10V12V13V26C2 26.5304 2.21071 27.0391 2.58579 27.4142C2.96086 27.7893 3.46957 28 4 28H28C28.5304 28 29.0391 27.7893 29.4142 27.4142C29.7893 27.0391 30 26.5304 30 26V13V12V10V9Z" fill="black"/>
<path d="M24 17.5C24 19.4891 23.2098 21.3968 21.8033 22.8033C20.3968 24.2098 18.4891 25 16.5 25C14.5109 25 12.6032 24.2098 11.1967 22.8033C9.79018 21.3968 9 19.4891 9 17.5C9 15.5109 9.79018 13.6032 11.1967 12.1967C12.6032 10.7902 14.5109 10 16.5 10C18.4891 10 20.3968 10.7902 21.8033 12.1967C23.2098 13.6032 24 15.5109 24 17.5V17.5ZM16.5 13.2812C16.5 13.1569 16.4506 13.0377 16.3627 12.9498C16.2748 12.8619 16.1556 12.8125 16.0312 12.8125C15.9069 12.8125 15.7877 12.8619 15.6998 12.9498C15.6119 13.0377 15.5625 13.1569 15.5625 13.2812V18.4375C15.5625 18.5201 15.5844 18.6013 15.6259 18.6727C15.6674 18.7442 15.727 18.8034 15.7987 18.8444L19.08 20.7194C19.1877 20.7776 19.3139 20.7913 19.4315 20.7577C19.5492 20.7241 19.6491 20.6458 19.7099 20.5396C19.7706 20.4333 19.7873 20.3075 19.7565 20.189C19.7257 20.0706 19.6498 19.9688 19.545 19.9056L16.5 18.1656V13.2812Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1,5 @@
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path d="M5 8C5.26522 8 5.51957 7.89464 5.70711 7.70711C5.89464 7.51957 6 7.26522 6 7C6 6.73478 5.89464 6.48043 5.70711 6.29289C5.51957 6.10536 5.26522 6 5 6C4.73478 6 4.48043 6.10536 4.29289 6.29289C4.10536 6.48043 4 6.73478 4 7C4 7.26522 4.10536 7.51957 4.29289 7.70711C4.48043 7.89464 4.73478 8 5 8ZM9 7C9 7.26522 8.89464 7.51957 8.70711 7.70711C8.51957 7.89464 8.26522 8 8 8C7.73478 8 7.48043 7.89464 7.29289 7.70711C7.10536 7.51957 7 7.26522 7 7C7 6.73478 7.10536 6.48043 7.29289 6.29289C7.48043 6.10536 7.73478 6 8 6C8.26522 6 8.51957 6.10536 8.70711 6.29289C8.89464 6.48043 9 6.73478 9 7ZM11 8C11.2652 8 11.5196 7.89464 11.7071 7.70711C11.8946 7.51957 12 7.26522 12 7C12 6.73478 11.8946 6.48043 11.7071 6.29289C11.5196 6.10536 11.2652 6 11 6C10.7348 6 10.4804 6.10536 10.2929 6.29289C10.1054 6.48043 10 6.73478 10 7C10 7.26522 10.1054 7.51957 10.2929 7.70711C10.4804 7.89464 10.7348 8 11 8V8Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.17157 3.17157C1.92172 2.42143 2.93913 2 4 2H28C29.0609 2 30.0783 2.42143 30.8284 3.17157C31.5786 3.92172 32 4.93913 32 6V26C32 27.0609 31.5786 28.0783 30.8284 28.8284C30.0783 29.5786 29.0609 30 28 30H4C2.93913 30 1.92172 29.5786 1.17157 28.8284C0.421427 28.0783 0 27.0609 0 26V6C0 4.93913 0.421427 3.92172 1.17157 3.17157ZM30 9V6C30 5.46957 29.7893 4.96086 29.4142 4.58579C29.0391 4.21071 28.5304 4 28 4H4C3.46957 4 2.96086 4.21071 2.58579 4.58579C2.21071 4.96086 2 5.46957 2 6V9V10V12V13V26C2 26.5304 2.21071 27.0391 2.58579 27.4142C2.96086 27.7893 3.46957 28 4 28H28C28.5304 28 29.0391 27.7893 29.4142 27.4142C29.7893 27.0391 30 26.5304 30 26V13V12V10V9Z" fill="black"/>
<path d="M20.7188 10H14.5444C14.1715 10.0003 13.8141 10.1487 13.5506 10.4125L11.2866 12.6747C11.156 12.8053 11.0525 12.9604 10.9818 13.1311C10.9112 13.3018 10.8749 13.4847 10.875 13.6694V23.5938C10.875 23.9667 11.0232 24.3244 11.2869 24.5881C11.5506 24.8518 11.9083 25 12.2813 25H20.7188C21.0917 25 21.4494 24.8518 21.7131 24.5881C21.9768 24.3244 22.125 23.9667 22.125 23.5938V11.4062C22.125 11.0333 21.9768 10.6756 21.7131 10.4119C21.4494 10.1482 21.0917 10 20.7188 10V10ZM14.1563 12.5781C14.3427 12.5781 14.5216 12.6522 14.6534 12.7841C14.7853 12.9159 14.8594 13.0948 14.8594 13.2812V15.1562C14.8594 15.3427 14.7853 15.5216 14.6534 15.6534C14.5216 15.7853 14.3427 15.8594 14.1563 15.8594C13.9698 15.8594 13.7909 15.7853 13.6591 15.6534C13.5272 15.5216 13.4531 15.3427 13.4531 15.1562V13.2812C13.4531 13.0948 13.5272 12.9159 13.6591 12.7841C13.7909 12.6522 13.9698 12.5781 14.1563 12.5781V12.5781ZM16.0312 12.5781C16.2177 12.5781 16.3966 12.6522 16.5284 12.7841C16.6603 12.9159 16.7344 13.0948 16.7344 13.2812V15.1562C16.7344 15.3427 16.6603 15.5216 16.5284 15.6534C16.3966 15.7853 16.2177 15.8594 16.0312 15.8594C15.8448 15.8594 15.6659 15.7853 15.5341 15.6534C15.4022 15.5216 15.3281 15.3427 15.3281 15.1562V13.2812C15.3281 13.0948 15.4022 12.9159 15.5341 12.7841C15.6659 12.6522 15.8448 12.5781 16.0312 12.5781V12.5781ZM18.6094 13.2812V15.1562C18.6094 15.3427 18.5353 15.5216 18.4034 15.6534C18.2716 15.7853 18.0927 15.8594 17.9062 15.8594C17.7198 15.8594 17.5409 15.7853 17.4091 15.6534C17.2772 15.5216 17.2031 15.3427 17.2031 15.1562V13.2812C17.2031 13.0948 17.2772 12.9159 17.4091 12.7841C17.5409 12.6522 17.7198 12.5781 17.9062 12.5781C18.0927 12.5781 18.2716 12.6522 18.4034 12.7841C18.5353 12.9159 18.6094 13.0948 18.6094 13.2812V13.2812ZM19.7812 12.5781C19.9677 12.5781 20.1466 12.6522 20.2784 12.7841C20.4103 12.9159 20.4844 13.0948 20.4844 13.2812V15.1562C20.4844 15.3427 20.4103 15.5216 20.2784 15.6534C20.1466 15.7853 19.9677 15.8594 19.7812 15.8594C19.5948 15.8594 19.4159 15.7853 19.2841 15.6534C19.1522 15.5216 19.0781 15.3427 19.0781 15.1562V13.2812C19.0781 13.0948 19.1522 12.9159 19.2841 12.7841C19.4159 12.6522 19.5948 12.5781 19.7812 12.5781V12.5781Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -0,0 +1,13 @@
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path d="M5 8C5.26522 8 5.51957 7.89464 5.70711 7.70711C5.89464 7.51957 6 7.26522 6 7C6 6.73478 5.89464 6.48043 5.70711 6.29289C5.51957 6.10536 5.26522 6 5 6C4.73478 6 4.48043 6.10536 4.29289 6.29289C4.10536 6.48043 4 6.73478 4 7C4 7.26522 4.10536 7.51957 4.29289 7.70711C4.48043 7.89464 4.73478 8 5 8ZM9 7C9 7.26522 8.89464 7.51957 8.70711 7.70711C8.51957 7.89464 8.26522 8 8 8C7.73478 8 7.48043 7.89464 7.29289 7.70711C7.10536 7.51957 7 7.26522 7 7C7 6.73478 7.10536 6.48043 7.29289 6.29289C7.48043 6.10536 7.73478 6 8 6C8.26522 6 8.51957 6.10536 8.70711 6.29289C8.89464 6.48043 9 6.73478 9 7ZM11 8C11.2652 8 11.5196 7.89464 11.7071 7.70711C11.8946 7.51957 12 7.26522 12 7C12 6.73478 11.8946 6.48043 11.7071 6.29289C11.5196 6.10536 11.2652 6 11 6C10.7348 6 10.4804 6.10536 10.2929 6.29289C10.1054 6.48043 10 6.73478 10 7C10 7.26522 10.1054 7.51957 10.2929 7.70711C10.4804 7.89464 10.7348 8 11 8V8Z" fill="black"/>
<g clip-path="url(#clip0_113_129)">
<path d="M14.0938 15.625C13.9694 15.625 13.8502 15.6744 13.7623 15.7623C13.6744 15.8502 13.625 15.9694 13.625 16.0938V18.9062C13.625 19.0306 13.6744 19.1498 13.7623 19.2377C13.8502 19.3256 13.9694 19.375 14.0938 19.375H16.9062C17.0306 19.375 17.1498 19.3256 17.2377 19.2377C17.3256 19.1498 17.375 19.0306 17.375 18.9062V16.0938C17.375 15.9694 17.3256 15.8502 17.2377 15.7623C17.1498 15.6744 17.0306 15.625 16.9062 15.625H14.0938Z" fill="black"/>
<path d="M13.1562 10.4688C13.1562 10.3444 13.1069 10.2252 13.019 10.1373C12.931 10.0494 12.8118 10 12.6875 10C12.5632 10 12.444 10.0494 12.356 10.1373C12.2681 10.2252 12.2188 10.3444 12.2188 10.4688V11.875C11.5971 11.875 11.001 12.1219 10.5615 12.5615C10.1219 13.001 9.875 13.5971 9.875 14.2188H8.46875C8.34443 14.2188 8.2252 14.2681 8.13729 14.356C8.04939 14.444 8 14.5632 8 14.6875C8 14.8118 8.04939 14.931 8.13729 15.019C8.2252 15.1069 8.34443 15.1562 8.46875 15.1562H9.875V16.0938H8.46875C8.34443 16.0938 8.2252 16.1431 8.13729 16.231C8.04939 16.319 8 16.4382 8 16.5625C8 16.6868 8.04939 16.806 8.13729 16.894C8.2252 16.9819 8.34443 17.0312 8.46875 17.0312H9.875V17.9688H8.46875C8.34443 17.9688 8.2252 18.0181 8.13729 18.106C8.04939 18.194 8 18.3132 8 18.4375C8 18.5618 8.04939 18.681 8.13729 18.769C8.2252 18.8569 8.34443 18.9062 8.46875 18.9062H9.875V19.8438H8.46875C8.34443 19.8438 8.2252 19.8931 8.13729 19.981C8.04939 20.069 8 20.1882 8 20.3125C8 20.4368 8.04939 20.556 8.13729 20.644C8.2252 20.7319 8.34443 20.7812 8.46875 20.7812H9.875C9.875 21.4029 10.1219 21.999 10.5615 22.4385C11.001 22.8781 11.5971 23.125 12.2188 23.125V24.5312C12.2188 24.6556 12.2681 24.7748 12.356 24.8627C12.444 24.9506 12.5632 25 12.6875 25C12.8118 25 12.931 24.9506 13.019 24.8627C13.1069 24.7748 13.1562 24.6556 13.1562 24.5312V23.125H14.0938V24.5312C14.0938 24.6556 14.1431 24.7748 14.231 24.8627C14.319 24.9506 14.4382 25 14.5625 25C14.6868 25 14.806 24.9506 14.894 24.8627C14.9819 24.7748 15.0312 24.6556 15.0312 24.5312V23.125H15.9688V24.5312C15.9688 24.6556 16.0181 24.7748 16.106 24.8627C16.194 24.9506 16.3132 25 16.4375 25C16.5618 25 16.681 24.9506 16.769 24.8627C16.8569 24.7748 16.9062 24.6556 16.9062 24.5312V23.125H17.8438V24.5312C17.8438 24.6556 17.8931 24.7748 17.981 24.8627C18.069 24.9506 18.1882 25 18.3125 25C18.4368 25 18.556 24.9506 18.644 24.8627C18.7319 24.7748 18.7812 24.6556 18.7812 24.5312V23.125C19.4029 23.125 19.999 22.8781 20.4385 22.4385C20.8781 21.999 21.125 21.4029 21.125 20.7812H22.5312C22.6556 20.7812 22.7748 20.7319 22.8627 20.644C22.9506 20.556 23 20.4368 23 20.3125C23 20.1882 22.9506 20.069 22.8627 19.981C22.7748 19.8931 22.6556 19.8438 22.5312 19.8438H21.125V18.9062H22.5312C22.6556 18.9062 22.7748 18.8569 22.8627 18.769C22.9506 18.681 23 18.5618 23 18.4375C23 18.3132 22.9506 18.194 22.8627 18.106C22.7748 18.0181 22.6556 17.9688 22.5312 17.9688H21.125V17.0312H22.5312C22.6556 17.0312 22.7748 16.9819 22.8627 16.894C22.9506 16.806 23 16.6868 23 16.5625C23 16.4382 22.9506 16.319 22.8627 16.231C22.7748 16.1431 22.6556 16.0938 22.5312 16.0938H21.125V15.1562H22.5312C22.6556 15.1562 22.7748 15.1069 22.8627 15.019C22.9506 14.931 23 14.8118 23 14.6875C23 14.5632 22.9506 14.444 22.8627 14.356C22.7748 14.2681 22.6556 14.2188 22.5312 14.2188H21.125C21.125 13.5971 20.8781 13.001 20.4385 12.5615C19.999 12.1219 19.4029 11.875 18.7812 11.875V10.4688C18.7812 10.3444 18.7319 10.2252 18.644 10.1373C18.556 10.0494 18.4368 10 18.3125 10C18.1882 10 18.069 10.0494 17.981 10.1373C17.8931 10.2252 17.8438 10.3444 17.8438 10.4688V11.875H16.9062V10.4688C16.9062 10.3444 16.8569 10.2252 16.769 10.1373C16.681 10.0494 16.5618 10 16.4375 10C16.3132 10 16.194 10.0494 16.106 10.1373C16.0181 10.2252 15.9688 10.3444 15.9688 10.4688V11.875H15.0312V10.4688C15.0312 10.3444 14.9819 10.2252 14.894 10.1373C14.806 10.0494 14.6868 10 14.5625 10C14.4382 10 14.319 10.0494 14.231 10.1373C14.1431 10.2252 14.0938 10.3444 14.0938 10.4688V11.875H13.1562V10.4688ZM14.0938 14.6875H16.9062C17.2792 14.6875 17.6369 14.8357 17.9006 15.0994C18.1643 15.3631 18.3125 15.7208 18.3125 16.0938V18.9062C18.3125 19.2792 18.1643 19.6369 17.9006 19.9006C17.6369 20.1643 17.2792 20.3125 16.9062 20.3125H14.0938C13.7208 20.3125 13.3631 20.1643 13.0994 19.9006C12.8357 19.6369 12.6875 19.2792 12.6875 18.9062V16.0938C12.6875 15.7208 12.8357 15.3631 13.0994 15.0994C13.3631 14.8357 13.7208 14.6875 14.0938 14.6875V14.6875Z" fill="black"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.17157 3.17157C1.92172 2.42143 2.93913 2 4 2H28C29.0609 2 30.0783 2.42143 30.8284 3.17157C31.5786 3.92172 32 4.93913 32 6V26C32 27.0609 31.5786 28.0783 30.8284 28.8284C30.0783 29.5786 29.0609 30 28 30H4C2.93913 30 1.92172 29.5786 1.17157 28.8284C0.421427 28.0783 0 27.0609 0 26V6C0 4.93913 0.421427 3.92172 1.17157 3.17157ZM30 9V6C30 5.46957 29.7893 4.96086 29.4142 4.58579C29.0391 4.21071 28.5304 4 28 4H4C3.46957 4 2.96086 4.21071 2.58579 4.58579C2.21071 4.96086 2 5.46957 2 6V9V10V12V13V26C2 26.5304 2.21071 27.0391 2.58579 27.4142C2.96086 27.7893 3.46957 28 4 28H28C28.5304 28 29.0391 27.7893 29.4142 27.4142C29.7893 27.0391 30 26.5304 30 26V13V12V10V9Z" fill="black"/>
<defs>
<clipPath id="clip0_113_129">
<rect width="15" height="15" fill="white" transform="translate(8 10)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-zoom-in" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z"/>
<path d="M10.344 11.742c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1 6.538 6.538 0 0 1-1.398 1.4z"/>
<path fill-rule="evenodd" d="M6.5 3a.5.5 0 0 1 .5.5V6h2.5a.5.5 0 0 1 0 1H7v2.5a.5.5 0 0 1-1 0V7H3.5a.5.5 0 0 1 0-1H6V3.5a.5.5 0 0 1 .5-.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 510 B

View file

@ -1,6 +1,6 @@
import Record from 'Types/Record';
import { FilterType, FilterKey, FilterCategory } from './filterType'
import { countries, platformOptions } from 'App/constants';
import filterOptions, { countries, platformOptions } from 'App/constants';
const countryOptions = Object.keys(countries).map(i => ({ text: countries[i], value: i }));
@ -19,198 +19,176 @@ const ISSUE_OPTIONS = [
{ text: 'JS Exception', value: 'js_exception' },
]
const filterKeys = ['is', 'isNot'];
const stringFilterKeys = ['is', 'isNot', 'contains', 'startsWith', 'endsWith', 'notContains'];
const targetFilterKeys = ['on', 'notOn', 'onAny'];
const signUpStatusFilterKeys = ['isSignedUp', 'notSignedUp'];
const rangeFilterKeys = ['before', 'after', 'on', 'inRange', 'notInRange', 'withInLast', 'notWithInLast'];
// const filterKeys = ['is', 'isNot'];
// const stringFilterKeys = ['is', 'isNot', 'contains', 'startsWith', 'endsWith', 'notContains'];
// const targetFilterKeys = ['on', 'notOn', 'onAny'];
// const signUpStatusFilterKeys = ['isSignedUp', 'notSignedUp'];
// const rangeFilterKeys = ['before', 'after', 'on', 'inRange', 'notInRange', 'withInLast', 'notWithInLast'];
const options = [
{
key: 'is',
text: 'is',
value: 'is'
}, {
key: 'isNot',
text: 'is not',
value: 'isNot'
}, {
key: 'startsWith',
text: 'starts with',
value: 'startsWith'
}, {
key: 'endsWith',
text: 'ends with',
value: 'endsWith'
}, {
key: 'contains',
text: 'contains',
value: 'contains'
}, {
key: 'notContains',
text: 'not contains',
value: 'notContains'
}, {
key: 'hasAnyValue',
text: 'has any value',
value: 'hasAnyValue'
}, {
key: 'hasNoValue',
text: 'has no value',
value: 'hasNoValue'
},
// const options = [
// {
// key: 'is',
// text: 'is',
// value: 'is'
// }, {
// key: 'isNot',
// text: 'is not',
// value: 'isNot'
// }, {
// key: 'startsWith',
// text: 'starts with',
// value: 'startsWith'
// }, {
// key: 'endsWith',
// text: 'ends with',
// value: 'endsWith'
// }, {
// key: 'contains',
// text: 'contains',
// value: 'contains'
// }, {
// key: 'notContains',
// text: 'not contains',
// value: 'notContains'
// }, {
// key: 'hasAnyValue',
// text: 'has any value',
// value: 'hasAnyValue'
// }, {
// key: 'hasNoValue',
// text: 'has no value',
// value: 'hasNoValue'
// },
// {
// key: 'isSignedUp',
// text: 'is signed up',
// value: 'isSignedUp'
// }, {
// key: 'notSignedUp',
// text: 'not signed up',
// value: 'notSignedUp'
// },
// {
// key: 'before',
// text: 'before',
// value: 'before'
// }, {
// key: 'after',
// text: 'after',
// value: 'after'
// }, {
// key: 'on',
// text: 'on',
// value: 'on'
// }, {
// key: 'notOn',
// text: 'not on',
// value: 'notOn'
// }, {
// key: 'inRage',
// text: 'in rage',
// value: 'inRage'
// }, {
// key: 'notInRage',
// text: 'not in rage',
// value: 'notInRage'
// }, {
// key: 'withinLast',
// text: 'within last',
// value: 'withinLast'
// }, {
// key: 'notWithinLast',
// text: 'not within last',
// value: 'notWithinLast'
// },
// {
// key: 'greaterThan',
// text: 'greater than',
// value: 'greaterThan'
// }, {
// key: 'lessThan',
// text: 'less than',
// value: 'lessThan'
// }, {
// key: 'equal',
// text: 'equal',
// value: 'equal'
// }, {
// key: 'not equal',
// text: 'not equal',
// value: 'not equal'
// },
// {
// key: 'onSelector',
// text: 'on selector',
// value: 'onSelector'
// }, {
// key: 'onText',
// text: 'on text',
// value: 'onText'
// }, {
// key: 'onComponent',
// text: 'on component',
// value: 'onComponent'
// },
// {
// key: 'onAny',
// text: 'on any',
// value: 'onAny'
// }
// ];
{
key: 'isSignedUp',
text: 'is signed up',
value: 'isSignedUp'
}, {
key: 'notSignedUp',
text: 'not signed up',
value: 'notSignedUp'
},
{
key: 'before',
text: 'before',
value: 'before'
}, {
key: 'after',
text: 'after',
value: 'after'
}, {
key: 'on',
text: 'on',
value: 'on'
}, {
key: 'notOn',
text: 'not on',
value: 'notOn'
}, {
key: 'inRage',
text: 'in rage',
value: 'inRage'
}, {
key: 'notInRage',
text: 'not in rage',
value: 'notInRage'
}, {
key: 'withinLast',
text: 'within last',
value: 'withinLast'
}, {
key: 'notWithinLast',
text: 'not within last',
value: 'notWithinLast'
},
// export const filterOptions = options.filter(({key}) => filterKeys.includes(key));
// export const filterOptions.stringOperators = options.filter(({key}) => stringFilterKeys.includes(key));
// export const filterOptions.targetOperators = options.filter(({key}) => targetFilterKeys.includes(key));
// export const booleanOptions = [
// { key: 'true', text: 'true', value: 'true' },
// { key: 'false', text: 'false', value: 'false' },
// ]
{
key: 'greaterThan',
text: 'greater than',
value: 'greaterThan'
}, {
key: 'lessThan',
text: 'less than',
value: 'lessThan'
}, {
key: 'equal',
text: 'equal',
value: 'equal'
}, {
key: 'not equal',
text: 'not equal',
value: 'not equal'
},
{
key: 'onSelector',
text: 'on selector',
value: 'onSelector'
}, {
key: 'onText',
text: 'on text',
value: 'onText'
}, {
key: 'onComponent',
text: 'on component',
value: 'onComponent'
},
{
key: 'onAny',
text: 'on any',
value: 'onAny'
}
];
export const filterOptions = options.filter(({key}) => filterKeys.includes(key));
export const stringFilterOptions = options.filter(({key}) => stringFilterKeys.includes(key));
export const targetFilterOptions = options.filter(({key}) => targetFilterKeys.includes(key));
export const booleanOptions = [
{ key: 'true', text: 'true', value: 'true' },
{ key: 'false', text: 'false', value: 'false' },
]
export const customOperators = [
{ key: '=', text: '=', value: '=' },
{ key: '<', text: '<', value: '<' },
{ key: '>', text: '>', value: '>' },
{ key: '<=', text: '<=', value: '<=' },
{ key: '>=', text: '>=', value: '>=' },
]
// export const filterOptions.customOperators = [
// { key: '=', text: '=', value: '=' },
// { key: '<', text: '<', value: '<' },
// { key: '>', text: '>', value: '>' },
// { key: '<=', text: '<=', value: '<=' },
// { key: '>=', text: '>=', value: '>=' },
// ]
export const filtersMap = {
[FilterKey.CLICK]: { key: FilterKey.CLICK, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Click', operator: 'on', operatorOptions: targetFilterOptions, icon: 'filters/click', isEvent: true },
[FilterKey.INPUT]: { key: FilterKey.INPUT, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Input', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/input', isEvent: true },
[FilterKey.LOCATION]: { key: FilterKey.LOCATION, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Page', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/location', isEvent: true },
[FilterKey.CUSTOM]: { key: FilterKey.CUSTOM, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Custom Events', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/custom', isEvent: true },
[FilterKey.FETCH]: { key: FilterKey.FETCH, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Fetch', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/fetch', isEvent: true },
[FilterKey.GRAPHQL]: { key: FilterKey.GRAPHQL, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'GraphQL', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/graphql', isEvent: true },
[FilterKey.STATEACTION]: { key: FilterKey.STATEACTION, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'StateAction', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/state-action', isEvent: true },
[FilterKey.ERROR]: { key: FilterKey.ERROR, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Error', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/error', isEvent: true },
// [FilterKey.METADATA]: { key: FilterKey.METADATA, type: FilterType.MULTIPLE, category: FilterCategory.METADATA, label: 'Metadata', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/metadata', isEvent: true },
// EVENTS
[FilterKey.CLICK]: { key: FilterKey.CLICK, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Click', operator: 'on', operatorOptions: filterOptions.targetOperators, icon: 'filters/click', isEvent: true },
[FilterKey.INPUT]: { key: FilterKey.INPUT, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Input', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/input', isEvent: true },
[FilterKey.LOCATION]: { key: FilterKey.LOCATION, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Page', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/location', isEvent: true },
[FilterKey.CUSTOM]: { key: FilterKey.CUSTOM, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Custom Events', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/custom', isEvent: true },
[FilterKey.FETCH]: { key: FilterKey.FETCH, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Fetch', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch', isEvent: true },
[FilterKey.GRAPHQL]: { key: FilterKey.GRAPHQL, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'GraphQL', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/graphql', isEvent: true },
[FilterKey.STATEACTION]: { key: FilterKey.STATEACTION, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'StateAction', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/state-action', isEvent: true },
[FilterKey.ERROR]: { key: FilterKey.ERROR, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Error', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/error', isEvent: true },
// [FilterKey.METADATA]: { key: FilterKey.METADATA, type: FilterType.MULTIPLE, category: FilterCategory.METADATA, label: 'Metadata', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/metadata', isEvent: true },
[FilterKey.USER_OS]: { key: FilterKey.USER_OS, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User OS', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/os' },
[FilterKey.USER_BROWSER]: { key: FilterKey.USER_BROWSER, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Browser', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/browser' },
[FilterKey.USER_DEVICE]: { key: FilterKey.USER_DEVICE, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Device', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/device' },
[FilterKey.PLATFORM]: { key: FilterKey.PLATFORM, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.GEAR, label: 'Platform', operator: 'is', operatorOptions: filterOptions, icon: 'filters/platform', options: platformOptions },
[FilterKey.REVID]: { key: FilterKey.REVID, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'RevId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/rev-id' },
[FilterKey.REFERRER]: { key: FilterKey.REFERRER, type: FilterType.MULTIPLE, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Referrer', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/referrer' },
[FilterKey.DURATION]: { key: FilterKey.DURATION, type: FilterType.DURATION, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Duration', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/duration' },
[FilterKey.USER_COUNTRY]: { key: FilterKey.USER_COUNTRY, type: FilterType.DROPDOWN, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'User Country', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/country', options: countryOptions },
// FILTERS
[FilterKey.USER_OS]: { key: FilterKey.USER_OS, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User OS', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/os' },
[FilterKey.USER_BROWSER]: { key: FilterKey.USER_BROWSER, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Browser', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/browser' },
[FilterKey.USER_DEVICE]: { key: FilterKey.USER_DEVICE, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Device', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/device' },
[FilterKey.PLATFORM]: { key: FilterKey.PLATFORM, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.GEAR, label: 'Platform', operator: 'is', operatorOptions: filterOptions.baseOperators, icon: 'filters/platform', options: platformOptions },
[FilterKey.REVID]: { key: FilterKey.REVID, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'RevId', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/rev-id' },
[FilterKey.REFERRER]: { key: FilterKey.REFERRER, type: FilterType.MULTIPLE, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Referrer', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/referrer' },
[FilterKey.DURATION]: { key: FilterKey.DURATION, type: FilterType.DURATION, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Duration', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/duration' },
[FilterKey.USER_COUNTRY]: { key: FilterKey.USER_COUNTRY, type: FilterType.DROPDOWN, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'User Country', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/country', options: countryOptions },
[FilterKey.CONSOLE]: { key: FilterKey.CONSOLE, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Console', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/console' },
[FilterKey.USERID]: { key: FilterKey.USERID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'UserId', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/userid' },
[FilterKey.USERANONYMOUSID]: { key: FilterKey.USERANONYMOUSID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'UserAnonymousId', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/userid' },
[FilterKey.CONSOLE]: { key: FilterKey.CONSOLE, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Console', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/console' },
[FilterKey.USERID]: { key: FilterKey.USERID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'UserId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/userid' },
[FilterKey.USERANONYMOUSID]: { key: FilterKey.USERANONYMOUSID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'UserAnonymousId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/userid' },
[FilterKey.DOM_COMPLETE]: { key: FilterKey.DOM_COMPLETE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'DOM Complete', operator: 'is', operatorOptions: stringFilterOptions, sourcesourceOperatorOptions: customOperators, source: [], icon: 'filters/click', isEvent: true },
[FilterKey.LARGEST_CONTENTFUL_PAINT_TIME]: { key: FilterKey.LARGEST_CONTENTFUL_PAINT_TIME, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Largest Contentful Paint Time', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click', isEvent: true },
// [FilterKey.TIME_BETWEEN_EVENTS]: { key: FilterKey.TIME_BETWEEN_EVENTS, type: FilterType.NUMBER, category: FilterCategory.PERFORMANCE, label: 'Time Between Events', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[FilterKey.TTFB]: { key: FilterKey.TTFB, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Time to First Byte', operator: 'is', operatorOptions: stringFilterOptions, sourceOperatorOptions: customOperators, source: [], icon: 'filters/click', isEvent: true },
[FilterKey.AVG_CPU_LOAD]: { key: FilterKey.AVG_CPU_LOAD, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg CPU Load', operator: 'is', operatorOptions: stringFilterOptions, sourceOperatorOptions: customOperators, source: [], icon: 'filters/click', isEvent: true },
[FilterKey.AVG_MEMORY_USAGE]: { key: FilterKey.AVG_MEMORY_USAGE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg Memory Usage', operator: 'is', operatorOptions: stringFilterOptions, sourceOperatorOptions: customOperators, source: [], icon: 'filters/click', isEvent: true },
[FilterKey.FETCH_FAILED]: { key: FilterKey.AVG_MEMORY_USAGE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Fetch Failed', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click', isEvent: true },
// [FilterKey.AVG_CPU_LOAD]: { key: FilterKey.AVG_CPU_LOAD, type: FilterType.NUMBER, category: 'new', label: 'Avg CPU Load', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
// [FilterKey.AVG_MEMORY_USAGE]: { key: FilterKey.AVG_MEMORY_USAGE, type: FilterType.NUMBER, category: 'new', label: 'Avg Memory Usage', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
// [FilterKey.SLOW_SESSION]: { key: FilterKey.SLOW_SESSION, type: FilterType.BOOLEAN, category: 'new', label: 'Slow Session', operator: 'true', operatorOptions: [{ key: 'true', text: 'true', value: 'true' }], icon: 'filters/click' },
// [FilterKey.MISSING_RESOURCE]: { key: FilterKey.MISSING_RESOURCE, type: FilterType.BOOLEAN, category: 'new', label: 'Missing Resource', operator: 'true', operatorOptions: [{ key: 'inImages', text: 'in images', value: 'true' }], icon: 'filters/click' },
// [FilterKey.CLICK_RAGE]: { key: FilterKey.CLICK_RAGE, type: FilterType.BOOLEAN, category: 'new', label: 'Click Rage', operator: 'onAnything', operatorOptions: [{ key: 'onAnything', text: 'on anything', value: 'true' }], icon: 'filters/click' },
[FilterKey.ISSUE]: { key: FilterKey.ISSUE, type: FilterType.ISSUE, category: FilterCategory.JAVASCRIPT, label: 'Issue', operator: 'is', operatorOptions: filterOptions, icon: 'filters/click', options: ISSUE_OPTIONS },
// [FilterKey.URL]: { / [TYPES,TYPES. category: 'interactions', label: 'URL', operator: 'is', operatorOptions: stringFilterOptions },
// [FilterKey.CUSTOM]: { / [TYPES,TYPES. category: 'interactions', label: 'Custom', operator: 'is', operatorOptions: stringFilterOptions },
// [FilterKey.METADATA]: { / [TYPES,TYPES. category: 'interactions', label: 'Metadata', operator: 'is', operatorOptions: stringFilterOptions },
// PERFORMANCE
[FilterKey.DOM_COMPLETE]: { key: FilterKey.DOM_COMPLETE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'DOM Complete', operator: 'is', operatorOptions: filterOptions.stringOperators, sourceOperatorOptions: filterOptions.customOperators, source: [], icon: 'filters/dom-complete', isEvent: true, hasSource: true, sourceOperator: '=', sourceType: FilterType.NUMBER },
[FilterKey.LARGEST_CONTENTFUL_PAINT_TIME]: { key: FilterKey.LARGEST_CONTENTFUL_PAINT_TIME, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Largest Contentful Paint Time', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/lcpt', isEvent: true },
// [FilterKey.TIME_BETWEEN_EVENTS]: { key: FilterKey.TIME_BETWEEN_EVENTS, type: FilterType.NUMBER, category: FilterCategory.PERFORMANCE, label: 'Time Between Events', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/click' },
[FilterKey.TTFB]: { key: FilterKey.TTFB, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Time to First Byte', operator: 'is', operatorOptions: filterOptions.stringOperators, sourceOperatorOptions: filterOptions.customOperators, source: [], icon: 'filters/ttfb', isEvent: true },
[FilterKey.AVG_CPU_LOAD]: { key: FilterKey.AVG_CPU_LOAD, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg CPU Load', operator: 'is', operatorOptions: filterOptions.stringOperators, sourceOperatorOptions: filterOptions.customOperators, source: [], icon: 'filters/cpu-load', isEvent: true },
[FilterKey.AVG_MEMORY_USAGE]: { key: FilterKey.AVG_MEMORY_USAGE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg Memory Usage', operator: 'is', operatorOptions: filterOptions.stringOperators, sourceOperatorOptions: filterOptions.customOperators, source: [], icon: 'filters/memory-load', isEvent: true },
[FilterKey.FETCH_FAILED]: { key: FilterKey.AVG_MEMORY_USAGE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Fetch Failed', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch-failed', isEvent: true },
[FilterKey.ISSUE]: { key: FilterKey.ISSUE, type: FilterType.ISSUE, category: FilterCategory.JAVASCRIPT, label: 'Issue', operator: 'is', operatorOptions: filterOptions.baseOperators, icon: 'filters/click', options: ISSUE_OPTIONS },
}
export default Record({
@ -226,52 +204,41 @@ export default Record({
custom: '',
// target: Target(),
level: '',
source: null,
hasNoValue: false,
isFilter: false,
actualValue: '',
operator: '',
hasSource: false,
source: [""],
sourceType: '',
sourceOperator: '=',
operatorOptions: [],
sourceOptions: [],
sourceOperatorOptions: [],
operator: '',
operatorOptions: [],
isEvent: false,
index: 0,
options: [],
}, {
keyKey: "_key",
fromJS: ({ key, ...filter }) => ({
...filter,
key,
type: filter.type, // camelCased(filter.type.toLowerCase()),
// key: filter.type === METADATA ? filter.label : filter.key || filter.type, // || camelCased(filter.type.toLowerCase()),
// label: getLabel(filter),
// target: Target(target),
// operator: getOperatorDefault(key),
// value: target ? target.label : filter.value,
// value: typeof value === 'string' ? [value] : value,
// icon: filter.type ? getfilterIcon(filter.type) : 'filters/metadata'
}),
fromJS: ({ value, type, ...filter }) => {
const _filter = filtersMap[type]
return {
...filter,
..._filter,
key: _filter.key,
type: _filter.type, // camelCased(filter.type.toLowerCase()),
value: value
}
},
})
// const NewFilterType = (key, category, icon, isEvent = false) => {
// return {
// key: key,
// category: category,
// label: filterMap[key].label,
// icon: icon,
// isEvent: isEvent,
// operators: filterMap[key].operatorOptions,
// value: [""]
// }
// }
const getOperatorDefault = (type) => {
if (type === MISSING_RESOURCE) return 'true';
if (type === SLOW_SESSION) return 'true';
if (type === CLICK_RAGE) return 'true';
if (type === CLICK) return 'on';
// const getOperatorDefault = (type) => {
// if (type === MISSING_RESOURCE) return 'true';
// if (type === SLOW_SESSION) return 'true';
// if (type === CLICK_RAGE) return 'true';
// if (type === CLICK) return 'on';
return 'is';
}
// return 'is';
// }

View file

@ -3,16 +3,16 @@ import Filter from './filter';
import { List } from 'immutable';
export default Record({
filterId: undefined,
searchId: undefined,
projectId: undefined,
userId: undefined,
name: undefined,
name: '',
filter: Filter(),
createdAt: undefined,
count: 0,
watchdogs: List()
}, {
idKey: 'filterId',
idKey: 'searchId',
methods: {
toData() {
const js = this.toJS();

22505
frontend/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -38,6 +38,7 @@ const babelLoader = {
],
plugins: [
"@babel/plugin-syntax-bigint",
["@babel/plugin-proposal-private-property-in-object", { "loose": true }],
[ '@babel/plugin-proposal-decorators', { legacy: true } ],
[ '@babel/plugin-proposal-class-properties', { loose: true }],
[ '@babel/plugin-proposal-private-methods', { loose: true }],

View file

@ -0,0 +1,12 @@
FROM openresty/openresty:buster
# Adding prometheus monitoring support
ADD https://raw.githubusercontent.com/knyar/nginx-lua-prometheus/master/prometheus.lua /usr/local/openresty/lualib/
ADD https://raw.githubusercontent.com/knyar/nginx-lua-prometheus/master/prometheus_keys.lua /usr/local/openresty/lualib/
ADD https://raw.githubusercontent.com/knyar/nginx-lua-prometheus/master/prometheus_resty_counter.lua /usr/local/openresty/lualib/
RUN chmod 0644 /usr/local/openresty/lualib/*.lua
# Enabling monitoring on port 9145
# Warning: don't expose this port to public network
COPY nginx.conf /usr/local/openresty${RESTY_DEB_FLAVOR}/nginx/conf/nginx.conf
RUN chmod 0644 /usr/local/openresty${RESTY_DEB_FLAVOR}/nginx/conf/nginx.conf

View file

@ -0,0 +1,122 @@
# nginx.conf -- docker-openresty
#
# This file is installed to:
# `/usr/local/openresty/nginx/conf/nginx.conf`
# and is the file loaded by nginx at startup,
# unless the user specifies otherwise.
#
# It tracks the upstream OpenResty's `nginx.conf`, but removes the `server`
# section and adds this directive:
# `include /etc/nginx/conf.d/*.conf;`
#
# The `docker-openresty` file `nginx.vh.default.conf` is copied to
# `/etc/nginx/conf.d/default.conf`. It contains the `server section
# of the upstream `nginx.conf`.
#
# See https://github.com/openresty/docker-openresty/blob/master/README.md#nginx-config-files
#
#user nobody;
#worker_processes 1;
# Enables the use of JIT for regular expressions to speed-up their processing.
pcre_jit on;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 10000;
}
http {
include mime.types;
default_type application/octet-stream;
# Enables or disables the use of underscores in client request header fields.
# When the use of underscores is disabled, request header fields whose names contain underscores are marked as invalid and become subject to the ignore_invalid_headers directive.
# underscores_in_headers off;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
# Log in JSON Format
# log_format nginxlog_json escape=json '{ "timestamp": "$time_iso8601", '
# '"remote_addr": "$remote_addr", '
# '"body_bytes_sent": $body_bytes_sent, '
# '"request_time": $request_time, '
# '"response_status": $status, '
# '"request": "$request", '
# '"request_method": "$request_method", '
# '"host": "$host",'
# '"upstream_addr": "$upstream_addr",'
# '"http_x_forwarded_for": "$http_x_forwarded_for",'
# '"http_referrer": "$http_referer", '
# '"http_user_agent": "$http_user_agent", '
# '"http_version": "$server_protocol", '
# '"nginx_access": true }';
# access_log /dev/stdout nginxlog_json;
# See Move default writable paths to a dedicated directory (#119)
# https://github.com/openresty/docker-openresty/issues/119
client_body_temp_path /var/run/openresty/nginx-client-body;
proxy_temp_path /var/run/openresty/nginx-proxy;
fastcgi_temp_path /var/run/openresty/nginx-fastcgi;
uwsgi_temp_path /var/run/openresty/nginx-uwsgi;
scgi_temp_path /var/run/openresty/nginx-scgi;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
# Enabling monitoring
lua_shared_dict prometheus_metrics 10M;
# lua_package_path "/path/to/nginx-lua-prometheus/?.lua;;";
init_worker_by_lua_block {
prometheus = require("prometheus").init("prometheus_metrics")
metric_requests = prometheus:counter(
"nginx_http_requests_total", "Number of HTTP requests", {"host", "status", "request_method"})
metric_latency = prometheus:histogram(
"nginx_http_request_duration_seconds", "HTTP request latency", {"host"})
metric_connections = prometheus:gauge(
"nginx_http_connections", "Number of HTTP connections", {"state"})
}
log_by_lua_block {
metric_requests:inc(1, {ngx.var.server_name, ngx.var.status, ngx.var.request_method})
metric_latency:observe(tonumber(ngx.var.request_time), {ngx.var.server_name})
}
server {
listen 9145;
location /metrics {
content_by_lua_block {
metric_connections:set(ngx.var.connections_reading, {"reading"})
metric_connections:set(ngx.var.connections_waiting, {"waiting"})
metric_connections:set(ngx.var.connections_writing, {"writing"})
prometheus:collect()
}
}
}
include /etc/nginx/conf.d/*.conf;
# Don't reveal OpenResty version to clients.
# server_tokens off;
}

View file

@ -0,0 +1,9 @@
BEGIN;
CREATE OR REPLACE FUNCTION openreplay_version()
RETURNS text AS
$$
SELECT 'v1.5.0'
$$ LANGUAGE sql IMMUTABLE;
ALTER TYPE public.error_source ADD VALUE IF NOT EXISTS 'elasticsearch';
COMMIT;

View file

@ -6,7 +6,7 @@ CREATE SCHEMA IF NOT EXISTS events;
CREATE OR REPLACE FUNCTION openreplay_version()
RETURNS text AS
$$
SELECT 'v1.4.0'
SELECT 'v1.5.0'
$$ LANGUAGE sql IMMUTABLE;
-- --- accounts.sql ---
@ -419,7 +419,7 @@ $$
-- --- errors.sql ---
CREATE TYPE error_source AS ENUM ('js_exception', 'bugsnag', 'cloudwatch', 'datadog', 'newrelic', 'rollbar', 'sentry', 'stackdriver', 'sumologic');
CREATE TYPE error_source AS ENUM ('js_exception', 'bugsnag', 'cloudwatch', 'datadog', 'newrelic', 'rollbar', 'sentry', 'stackdriver', 'sumologic', 'elasticsearch');
CREATE TYPE error_status AS ENUM ('unresolved', 'resolved', 'ignored');
CREATE TABLE errors
(

View file

@ -1 +0,0 @@
../helm/openreplay-cli

144
scripts/helmcharts/openreplay-cli Executable file
View file

@ -0,0 +1,144 @@
#!/bin/bash
## This script is a helper for managing your OpenReplay instance
set -eE -o pipefail # same as: `set -o errexit -o errtrace`
# Trapping the error
trap err EXIT
err() {
case "$?" in
0)
;;
*)
;;
esac
}
# make all stderr red
color()(set -o pipefail;"$@" 2>&1>&3|sed $'s,.*,\e[31m&\e[m,'>&2)3>&1
# color schemes
# Ansi color code variables
red="\e[0;91m"
blue="\e[0;94m"
expand_bg="\e[K"
blue_bg="\e[0;104m${expand_bg}"
red_bg="\e[0;101m${expand_bg}"
green_bg="\e[0;102m${expand_bg}"
green="\e[0;92m"
white="\e[0;97m"
bold="\e[1m"
uline="\e[4m"
reset="\e[0m"
CWD=$pwd
usage()
{
clear
cat <<"EOF"
___ ____ _
/ _ \ _ __ ___ _ __ | _ \ ___ _ __ | | __ _ _ _
| | | | '_ \ / _ \ '_ \| |_) / _ \ '_ \| |/ _` | | | |
| |_| | |_) | __/ | | | _ < __/ |_) | | (_| | |_| |
\___/| .__/ \___|_| |_|_| \_\___| .__/|_|\__,_|\__, |
|_| |_| |___/
EOF
echo -e "${green}Usage: openreplay-cli [ -h | --help ]
[ -d | --status ]
[ -v | --verbose ]
[ -l | --logs SERVICE ]
[ -I | --helm-install SERVICE ]
[ -s | --stop SERVICE|all ]
[ -S | --start SERVICE|all ]
[ -r | --restart SERVICE|all ]"
echo -e "${reset}${blue}services: ${services[*]}${reset}"
exit 0
}
services=( alerts assets chalice clickhouse ender sink storage http integrations ios-proxy db pg redis postgresql )
check() {
if ! command -v kubectl &> /dev/null
then
>&2 echo "Kubectl not found. Please refer https://kubernetes.io/docs/tasks/tools/install-kubectl/ "
exit 2
fi
kubectl cluster-info &> /dev/null
if [[ $? -ne 0 ]]; then
echo -e "${red}Kubernetes cluster is not accessible.\nPlease check ${bold}KUBECONFIG${reset}${red} env variable is set or ${bold}~/.kube/config exists.${reset}"
exit 1
fi
}
stop() {
if [[ $1 == "all" ]]; then
kubectl scale deployment -n app --replicas=0 --all
return
fi
kubectl scale -n app deployment --replicas=0 $1-openreplay
}
start() {
helm upgrade --install openreplay -n app openreplay -f vars.yaml
}
restart() {
if [[ $1 == "all" ]]; then
kubectl rollout restart deployment -n app
return
fi
kubectl rollout restart -n app deployment $1-openreplay
}
helmInstall() {
helm upgrade --install openreplay -n app openreplay -f vars.yaml
}
upgrade() {
sed -i "s/tag:.*/ tag: 'latest'/g" ./app/$1.yaml
}
logs() {
check
kubectl logs --timestamps -n app -l app.kubernetes.io/name=$1 -f
}
status() {
kubectl get deployment.apps -n app
}
[[ $# -eq 0 ]] && usage && exit 1
PARSED_ARGUMENTS=$(color getopt -a -n openreplay-cli -o vhds:S:l:r:I --long verbose,help,status,start:,stop:,logs:,restart:,helm-install -- "$@")
VALID_ARGUMENTS=$?
if [[ "$VALID_ARGUMENTS" != "0" ]]; then
usage
fi
eval set -- "$PARSED_ARGUMENTS"
while :
do
case "$1" in
-v | --verbose) VERBOSE=1 ; shift ;;
-h | --help) usage ; shift ;;
-d | --status) status ; shift ;;
-I | --helm-install) helmInstall; shift ;;
-s | --stop) stop $2 ; shift 2 ;;
-S | --start) start $2 ; shift 2 ;;
-l | --logs) logs "$2" ; shift 2 ;;
-r | --restart) restart "$2" ; shift 2 ;;
# -- means the end of the arguments; drop this, and break out of the while loop
--) shift; break ;;
# If invalid options were passed, then getopt should have reported an error,
# which we checked as VALID_ARGUMENTS when getopt was called...
*) echo "Unexpected option: $1 - this should not happen."
usage ;;
esac
done
[[ $VERBOSE -eq 1 ]] && set -x

View file

@ -69,7 +69,7 @@ resources: {}
# memory: 128Mi
autoscaling:
enabled: true
enabled: false
minReplicas: 1
maxReplicas: 5
targetCPUUtilizationPercentage: 80

View file

@ -69,7 +69,7 @@ resources: {}
# memory: 128Mi
autoscaling:
enabled: true
enabled: false
minReplicas: 1
maxReplicas: 5
targetCPUUtilizationPercentage: 80

View file

@ -69,7 +69,7 @@ resources: {}
# memory: 128Mi
autoscaling:
enabled: true
enabled: false
minReplicas: 1
maxReplicas: 5
targetCPUUtilizationPercentage: 80

View file

@ -69,7 +69,7 @@ resources: {}
# memory: 128Mi
autoscaling:
enabled: true
enabled: false
minReplicas: 1
maxReplicas: 5
targetCPUUtilizationPercentage: 80

View file

@ -69,7 +69,7 @@ resources: {}
# memory: 128Mi
autoscaling:
enabled: true
enabled: false
minReplicas: 1
maxReplicas: 5
targetCPUUtilizationPercentage: 80

View file

@ -69,7 +69,7 @@ resources: {}
# memory: 128Mi
autoscaling:
enabled: true
enabled: false
minReplicas: 1
maxReplicas: 5
targetCPUUtilizationPercentage: 80
@ -78,7 +78,7 @@ autoscaling:
env:
TOKEN_SECRET: secret_token_string # TODO: generate on buld
S3_BUCKET_IOS_IMAGES: sessions-mobile-assets
CACHE_ASSETS: false
CACHE_ASSETS: true
HTTP_PORT: 80

View file

@ -69,7 +69,7 @@ resources: {}
# memory: 128Mi
autoscaling:
enabled: true
enabled: false
minReplicas: 1
maxReplicas: 5
targetCPUUtilizationPercentage: 80

View file

@ -71,8 +71,18 @@ data:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://utilities-openreplay.app.svc.cluster.local:9000;
}
location /ws-assist/ {
rewrite ^/ws-assist/(.*) /$1 break;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://utilities-openreplay.app.svc.cluster.local:9001;
}
location /assets/ {
rewrite ^/assets/(.*) /sessions-assets/$1 break;
proxy_http_version 1.1;
@ -140,16 +150,25 @@ data:
default $http_x_forwarded_proto;
'' $scheme;
}
# Default server for helath check
server {
listen 80 default_server;
listen [::]:80 default_server;
# server_name _;
listen [::]:80;
location /healthz {
return 200 'OK';
}
}
server {
listen 80;
listen [::]:80;
server_name {{ .Values.global.domainName }};
{{ .Values.customServerConfigs }}
include /etc/nginx/conf.d/location.list;
client_max_body_size 10M;
}
server {
listen 443 ssl;
server_name {{ .Values.global.domainName }};
ssl_certificate /etc/secrets/site.crt;
ssl_certificate_key /etc/secrets/site.key;
ssl_protocols TLSv1.2 TLSv1.3;

View file

@ -41,6 +41,9 @@ spec:
- name: http
containerPort: 80
protocol: TCP
- name: metrics
containerPort: 9145
protocol: TCP
livenessProbe:
httpGet:
path: /healthz

View file

@ -19,3 +19,19 @@ spec:
{{- end }}
selector:
{{- include "nginx-ingress.selectorLabels" . | nindent 4 }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "nginx-ingress.fullname" . }}-metrics
labels:
{{- include "nginx-ingress.labels" . | nindent 4 }}
openreplay/monitoring: nginx-ingress-metrics
spec:
selector:
{{- include "nginx-ingress.selectorLabels" . | nindent 4 }}
ports:
- name: metrics
port: 9145
targetPort: 9145
protocol: TCP

View file

@ -69,7 +69,7 @@ resources: {}
# memory: 128Mi
autoscaling:
enabled: true
enabled: false
minReplicas: 1
maxReplicas: 5
targetCPUUtilizationPercentage: 80

View file

@ -69,7 +69,7 @@ resources: {}
# memory: 128Mi
autoscaling:
enabled: true
enabled: false
minReplicas: 1
maxReplicas: 5
targetCPUUtilizationPercentage: 80

View file

@ -55,9 +55,11 @@ spec:
value: '{{ $val }}'
{{- end}}
ports:
- name: http
containerPort: {{ .Values.service.port }}
{{- range $key, $val := .Values.service.ports }}
- name: {{ $key }}
containerPort: {{ $val }}
protocol: TCP
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}

View file

@ -7,9 +7,11 @@ metadata:
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
{{- range $key, $val := .Values.service.ports }}
- port: {{ $val }}
targetPort: {{ $key }}
protocol: TCP
name: http
name: {{ $key }}
{{- end}}
selector:
{{- include "utilities.selectorLabels" . | nindent 4 }}

View file

@ -36,9 +36,15 @@ securityContext: {}
# runAsNonRoot: true
# runAsUser: 1000
#service:
# type: ClusterIP
# port: 9000
service:
type: ClusterIP
port: 9000
ports:
peerjs: 9000
socketio: 9001
ingress:
enabled: false
@ -69,7 +75,7 @@ resources: {}
# memory: 128Mi
autoscaling:
enabled: true
enabled: false
minReplicas: 1
maxReplicas: 5
targetCPUUtilizationPercentage: 80

View file

@ -87,7 +87,7 @@ nginx-ingress:
# Using openresty because we need lua support,
# and dynamic resolve config from local.
# Ref: https://serverfault.com/questions/638822/nginx-resolver-address-from-etc-resolv-conf
repository: openresty/openresty
repository: rg.fr-par.scw.cloud/foss/openresty
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: "buster"

View file

@ -12,9 +12,18 @@
"aws-sdk": "^2.992.0",
"express": "^4.17.1",
"peer": "^0.6.1",
"socket.io": "^4.4.1",
"source-map": "^0.7.3"
}
},
"node_modules/@socket.io/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ==",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/@types/body-parser": {
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
@ -24,6 +33,11 @@
"@types/node": "*"
}
},
"node_modules/@types/component-emitter": {
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz",
"integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ=="
},
"node_modules/@types/connect": {
"version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
@ -32,6 +46,11 @@
"@types/node": "*"
}
},
"node_modules/@types/cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
},
"node_modules/@types/cors": {
"version": "2.8.12",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz",
@ -172,6 +191,14 @@
}
]
},
"node_modules/base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
"engines": {
"node": "^4.5.0 || >= 5.9"
}
},
"node_modules/body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
@ -244,6 +271,11 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
},
"node_modules/content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
@ -335,6 +367,86 @@
"node": ">= 0.8"
}
},
"node_modules/engine.io": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.2.tgz",
"integrity": "sha512-v/7eGHxPvO2AWsksyx2PUsQvBafuvqs0jJJQ0FdmJG1b9qIvgSbqDRGwNhfk2XHaTTbTXiC4quRE8Q9nRjsrQQ==",
"dependencies": {
"@types/cookie": "^0.4.1",
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.4.1",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.0",
"ws": "~8.2.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/engine.io-parser": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz",
"integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==",
"dependencies": {
"@socket.io/base64-arraybuffer": "~1.0.2"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/engine.io/node_modules/cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/engine.io/node_modules/debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/engine.io/node_modules/ws": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@ -829,6 +941,82 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"node_modules/socket.io": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.1.tgz",
"integrity": "sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg==",
"dependencies": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"debug": "~4.3.2",
"engine.io": "~6.1.0",
"socket.io-adapter": "~2.3.3",
"socket.io-parser": "~4.0.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-adapter": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz",
"integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ=="
},
"node_modules/socket.io-parser": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz",
"integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==",
"dependencies": {
"@types/component-emitter": "^1.2.10",
"component-emitter": "~1.3.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-parser/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/socket.io/node_modules/debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
@ -1026,6 +1214,11 @@
}
},
"dependencies": {
"@socket.io/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ=="
},
"@types/body-parser": {
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
@ -1035,6 +1228,11 @@
"@types/node": "*"
}
},
"@types/component-emitter": {
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz",
"integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ=="
},
"@types/connect": {
"version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
@ -1043,6 +1241,11 @@
"@types/node": "*"
}
},
"@types/cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
},
"@types/cors": {
"version": "2.8.12",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz",
@ -1154,6 +1357,11 @@
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
},
"body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
@ -1214,6 +1422,11 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
},
"content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
@ -1284,6 +1497,57 @@
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"engine.io": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.2.tgz",
"integrity": "sha512-v/7eGHxPvO2AWsksyx2PUsQvBafuvqs0jJJQ0FdmJG1b9qIvgSbqDRGwNhfk2XHaTTbTXiC4quRE8Q9nRjsrQQ==",
"requires": {
"@types/cookie": "^0.4.1",
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.4.1",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.0",
"ws": "~8.2.3"
},
"dependencies": {
"cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
},
"debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"requires": {
"ms": "2.1.2"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"ws": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
"requires": {}
}
}
},
"engine.io-parser": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz",
"integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==",
"requires": {
"@socket.io/base64-arraybuffer": "~1.0.2"
}
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@ -1660,6 +1924,64 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"socket.io": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.1.tgz",
"integrity": "sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg==",
"requires": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"debug": "~4.3.2",
"engine.io": "~6.1.0",
"socket.io-adapter": "~2.3.3",
"socket.io-parser": "~4.0.4"
},
"dependencies": {
"debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"requires": {
"ms": "2.1.2"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"socket.io-adapter": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz",
"integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ=="
},
"socket.io-parser": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz",
"integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==",
"requires": {
"@types/component-emitter": "^1.2.10",
"component-emitter": "~1.3.0",
"debug": "~4.3.1"
},
"dependencies": {
"debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"requires": {
"ms": "2.1.2"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",

View file

@ -21,6 +21,7 @@
"aws-sdk": "^2.992.0",
"express": "^4.17.1",
"peer": "^0.6.1",
"socket.io": "^4.4.1",
"source-map": "^0.7.3"
}
}

View file

@ -2,24 +2,38 @@ var sourcemapsReaderServer = require('./servers/sourcemaps-server');
var {peerRouter, peerConnection, peerDisconnect, peerError} = require('./servers/peerjs-server');
var express = require('express');
const {ExpressPeerServer} = require('peer');
const socket = require("./servers/websocket");
const HOST = '0.0.0.0';
const PORT = 9000;
var app = express();
app.use((req, res, next) => {
console.log(new Date().toTimeString(), req.method, req.originalUrl);
next();
});
var wsapp = express();
const request_logger = (identity) => {
return (req, res, next) => {
console.log(identity,new Date().toTimeString(), 'REQUEST', req.method, req.originalUrl);
res.on('finish', function () {
console.log(new Date().toTimeString(), 'RESPONSE', req.method, req.originalUrl, this.statusCode);
})
next();
}
};
app.use(request_logger("[app]"));
wsapp.use(request_logger("[wsapp]"));
app.use('/sourcemaps', sourcemapsReaderServer);
app.use('/assist', peerRouter);
wsapp.use('/assist', socket.wsRouter);
const server = app.listen(PORT, HOST, () => {
console.log(`App listening on http://${HOST}:${PORT}`);
console.log('Press Ctrl+C to quit.');
});
const wsserver = wsapp.listen(PORT + 1, HOST, () => {
console.log(`WS App listening on http://${HOST}:${PORT + 1}`);
console.log('Press Ctrl+C to quit.');
});
const peerServer = ExpressPeerServer(server, {
debug: true,
path: '/',
@ -31,4 +45,6 @@ peerServer.on('disconnect', peerDisconnect);
peerServer.on('error', peerError);
app.use('/', peerServer);
app.enable('trust proxy');
module.exports = server;
wsapp.enable('trust proxy');
socket.start(wsserver);
module.exports = {wsserver, server};

View file

@ -66,5 +66,6 @@ module.exports = {
peerRouter,
peerConnection,
peerDisconnect,
peerError
peerError,
extractPeerId
};

View file

@ -0,0 +1,157 @@
const _io = require('socket.io');
var express = require('express');
var {extractPeerId} = require('./peerjs-server');
var wsRouter = express.Router();
const IDENTITIES = {agent: 'agent', session: 'session'};
const NEW_AGENT_MESSAGE = "NEW_AGENT";
const NO_AGENTS = "NO_AGENT";
const NO_SESSIONS = "SESSION_DISCONNECTED";
const SESSION_ALREADY_CONNECTED = "SESSION_ALREADY_CONNECTED";
// const wsReconnectionTimeout = process.env.wsReconnectionTimeout | 10 * 1000;
let connectedSessions = {};
wsRouter.get(`/${process.env.S3_KEY}/sockets-list`, function (req, res) {
console.log("[WS]looking for all available sessions");
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({"data": connectedSessions}));
});
wsRouter.get(`/${process.env.S3_KEY}/sockets-list/:projectKey`, function (req, res) {
console.log(`[WS]looking for available sessions for ${req.params.projectKey}`);
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({"data": connectedSessions[req.params.projectKey] || []}));
});
const removeSession = (projectKey, sessionId) => {
const i = (connectedSessions[projectKey] || []).indexOf(sessionId);
if (i !== -1) {
connectedSessions[projectKey].splice(i, 1);
}
};
const findSessionSocketId = async (io, peerId) => {
const connected_sockets = await io.in(peerId).fetchSockets();
for (let item of connected_sockets) {
if (item.handshake.query.identity === IDENTITIES.session) {
return item.id;
}
}
return null;
};
async function sessions_agents_count(io, socket) {
let c_sessions = 0, c_agents = 0;
if (io.sockets.adapter.rooms.get(socket.peerId)) {
const connected_sockets = await io.in(socket.peerId).fetchSockets();
for (let item of connected_sockets) {
if (item.handshake.query.identity === IDENTITIES.session) {
c_sessions++;
} else {
c_agents++;
}
}
} else {
c_agents = -1;
c_sessions = -1;
}
return {c_sessions, c_agents};
}
module.exports = {
wsRouter,
start: (server) => {
const io = _io(server, {
maxHttpBufferSize: 7e6,
cors: {
origin: "*",
methods: ["GET", "POST", "PUT"]
},
path: '/socket'
});
io.on('connection', async (socket) => {
console.log(`WS started:${socket.id}, Query:${JSON.stringify(socket.handshake.query)}`);
socket.peerId = socket.handshake.query.peerId;
socket.identity = socket.handshake.query.identity;
const {projectKey, sessionId} = extractPeerId(socket.peerId);
socket.sessionId = sessionId;
socket.projectKey = projectKey;
socket.lastMessageReceivedAt = Date.now();
let {c_sessions, c_agents} = await sessions_agents_count(io, socket);
if (socket.identity === IDENTITIES.session) {
connectedSessions[socket.projectKey] = connectedSessions[socket.projectKey] || [];
if (!connectedSessions[socket.projectKey].includes(socket.sessionId)) {
connectedSessions[socket.projectKey].push(socket.sessionId);
}
if (c_sessions > 0) {
console.log(`session already connected, refusing new connexion`);
io.to(socket.id).emit(SESSION_ALREADY_CONNECTED);
return socket.disconnect();
}
if (c_agents > 0) {
console.log(`notifying new session about agent-existance`);
io.to(socket.id).emit(NEW_AGENT_MESSAGE);
}
} else if (c_sessions <= 0) {
console.log(`notifying new agent about no SESSIONS`);
io.to(socket.id).emit(NO_SESSIONS);
}
socket.join(socket.peerId);
if (io.sockets.adapter.rooms.get(socket.peerId)) {
console.log(`${socket.id} joined room:${socket.peerId}, as:${socket.identity}, size:${io.sockets.adapter.rooms.get(socket.peerId).size}`);
}
socket.on('disconnect', async () => {
// console.log(`${socket.id} disconnected from ${socket.peerId}, waiting ${wsReconnectionTimeout / 1000}s before checking remaining`);
console.log(`${socket.id} disconnected from ${socket.peerId}`);
// wait a little bit before notifying everyone
// setTimeout(async () => {
console.log("checking for number of connected agents and sessions");
let {c_sessions, c_agents} = await sessions_agents_count(io, socket);
if (c_sessions === -1 && c_agents === -1) {
console.log(`room not found: ${socket.peerId}`);
return removeSession(socket.projectKey, socket.sessionId);
}
if (c_sessions === 0) {
console.log(`notifying everyone in ${socket.peerId} about no SESSIONS`);
socket.to(socket.peerId).emit(NO_SESSIONS);
removeSession(socket.projectKey, socket.sessionId);
}
if (c_agents === 0) {
console.log(`notifying everyone in ${socket.peerId} about no AGENTS`);
socket.to(socket.peerId).emit(NO_AGENTS);
}
// }, wsReconnectionTimeout);
});
socket.onAny(async (eventName, ...args) => {
socket.lastMessageReceivedAt = Date.now();
if (socket.identity === IDENTITIES.session) {
console.log(`received event:${eventName}, from:${socket.identity}, sending message to room:${socket.peerId}, size: ${io.sockets.adapter.rooms.get(socket.peerId).size}`);
socket.to(socket.peerId).emit(eventName, args[0]);
} else {
console.log(`received event:${eventName}, from:${socket.identity}, sending message to session of room:${socket.peerId}, size:${io.sockets.adapter.rooms.get(socket.peerId).size}`);
let socketId = await findSessionSocketId(io, socket.peerId);
if (socketId === null) {
console.log(`session not found for:${socket.peerId}`);
io.to(socket.id).emit(NO_SESSIONS);
} else {
console.log("message sent");
io.to(socketId).emit(eventName, args[0]);
}
}
});
if (socket.identity === IDENTITIES.agent) {
socket.to(socket.peerId).emit(NEW_AGENT_MESSAGE);
}
});
}
};