Merge pull request #694 from openreplay/adopted-style-sheets

Tracker 3.6.0 and message schema update
* feat (tracker, backend, player): Adopted Style Sheets maintenance
* refactor(tracker,player): compact messages representation (as array)
* feat (tracker,backend): Use real sessionStart timestamp decoded from token on start
* fix (frontend/assist): Fix activity timeouts logic in assist
* fix (tracker): maintain scroll, mousemove, mouseclick, exceptions inside iFrames
* fix (tracker): img module url resolving
* fix (tracker): critical bug in observer (missing nodes)
* feat (tracker): sessionHash returned on stop can be used for continuing session on start
This commit is contained in:
Alex K 2022-08-26 16:00:31 +02:00 committed by GitHub
commit f19a7df354
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 2606 additions and 913 deletions

1
backend/cmd/assets/file Normal file
View file

@ -0,0 +1 @@
GROUP_CACHE=from_file

View file

@ -0,0 +1,92 @@
chalice:
env:
jwt_secret: SetARandomStringHere
clickhouse:
enabled: false
fromVersion: v1.6.0
global:
domainName: openreplay.local
email:
emailFrom: OpenReplay<do-not-reply@openreplay.com>
emailHost: ""
emailPassword: ""
emailPort: "587"
emailSslCert: ""
emailSslKey: ""
emailUseSsl: "false"
emailUseTls: "true"
emailUser: ""
enterpriseEditionLicense: ""
ingress:
controller:
config:
enable-real-ip: true
force-ssl-redirect: false
max-worker-connections: 0
proxy-body-size: 10m
ssl-redirect: false
extraArgs:
default-ssl-certificate: app/openreplay-ssl
ingressClass: openreplay
ingressClassResource:
name: openreplay
service:
externalTrafficPolicy: Local
kafka:
kafkaHost: kafka.db.svc.cluster.local
kafkaPort: "9092"
kafkaUseSsl: "false"
zookeeperHost: databases-zookeeper.svc.cluster.local
zookeeperNonTLSPort: 2181
postgresql:
postgresqlDatabase: postgres
postgresqlHost: postgresql.db.svc.cluster.local
postgresqlPassword: changeMePassword
postgresqlPort: "5432"
postgresqlUser: postgres
redis:
redisHost: redis-master.db.svc.cluster.local
redisPort: "6379"
s3:
accessKey: changeMeMinioAccessKey
assetsBucket: sessions-assets
endpoint: http://minio.db.svc.cluster.local:9000
recordingsBucket: mobs
region: us-east-1
secretKey: changeMeMinioPassword
sourcemapsBucket: sourcemaps
ingress-nginx:
controller:
config:
enable-real-ip: true
force-ssl-redirect: false
max-worker-connections: 0
proxy-body-size: 10m
ssl-redirect: false
extraArgs:
default-ssl-certificate: app/openreplay-ssl
ingressClass: openreplay
ingressClassResource:
name: openreplay
service:
externalTrafficPolicy: Local
kafka:
kafkaHost: kafka.db.svc.cluster.local
kafkaPort: "9092"
kafkaUseSsl: "false"
zookeeperHost: databases-zookeeper.svc.cluster.local
zookeeperNonTLSPort: 2181
minio:
global:
minio:
accessKey: changeMeMinioAccessKey
secretKey: changeMeMinioPassword
postgresql:
postgresqlDatabase: postgres
postgresqlHost: postgresql.db.svc.cluster.local
postgresqlPassword: changeMePassword
postgresqlPort: "5432"
postgresqlUser: postgres
redis:
redisHost: redis-master.db.svc.cluster.local
redisPort: "6379"

View file

@ -57,6 +57,21 @@ func (e *AssetsCache) ParseAssets(sessID uint64, msg messages.Message) messages.
}
newMsg.SetMeta(msg.Meta())
return newMsg
case *messages.AdoptedSSReplaceURLBased:
newMsg := &messages.AdoptedSSReplace{
SheetID: m.SheetID,
Text: e.handleCSS(sessID, m.BaseURL, m.Text),
}
newMsg.SetMeta(msg.Meta())
return newMsg
case *messages.AdoptedSSInsertRuleURLBased:
newMsg := &messages.AdoptedSSInsertRule{
SheetID: m.SheetID,
Index: m.Index,
Rule: e.handleCSS(sessID, m.BaseURL, m.Rule),
}
newMsg.SetMeta(msg.Meta())
return newMsg
}
return msg
}

View file

@ -2,7 +2,7 @@
package messages
func IsReplayerType(id int) bool {
return 0 == id || 2 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 22 == id || 37 == id || 38 == id || 39 == id || 40 == id || 41 == id || 44 == id || 45 == id || 46 == id || 47 == id || 48 == id || 49 == id || 54 == id || 55 == id || 59 == id || 69 == id || 70 == id || 90 == id || 93 == id || 96 == id || 100 == id || 102 == id || 103 == id || 105 == id
return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 22 == id || 37 == id || 38 == id || 39 == id || 40 == id || 41 == id || 44 == id || 45 == id || 46 == id || 47 == id || 48 == id || 49 == id || 54 == id || 55 == id || 59 == id || 60 == id || 61 == id || 67 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 90 == id || 93 == id || 96 == id || 100 == id || 102 == id || 103 == id || 105 == id
}
func IsIOSType(id int) bool {

View file

@ -14,8 +14,6 @@ const (
MsgSessionStart = 1
MsgSessionDisconnect = 2
MsgSessionEnd = 3
MsgSetPageLocation = 4
@ -144,6 +142,20 @@ const (
MsgCreateIFrameDocument = 70
MsgAdoptedSSReplaceURLBased = 71
MsgAdoptedSSReplace = 72
MsgAdoptedSSInsertRuleURLBased = 73
MsgAdoptedSSInsertRule = 74
MsgAdoptedSSDeleteRule = 75
MsgAdoptedSSAddOwner = 76
MsgAdoptedSSRemoveOwner = 77
MsgIOSBatchMeta = 107
MsgIOSSessionStart = 90
@ -387,38 +399,6 @@ func (msg *SessionStart) TypeID() int {
return 1
}
type SessionDisconnect struct {
message
Timestamp uint64
}
func (msg *SessionDisconnect) Encode() []byte {
buf := make([]byte, 11)
buf[0] = 2
p := 1
p = WriteUint(msg.Timestamp, buf, p)
return buf[:p]
}
func (msg *SessionDisconnect) EncodeWithIndex() []byte {
encoded := msg.Encode()
if IsIOSType(msg.TypeID()) {
return encoded
}
data := make([]byte, len(encoded)+8)
copy(data[8:], encoded[:])
binary.LittleEndian.PutUint64(data[0:], msg.Meta().Index)
return data
}
func (msg *SessionDisconnect) Decode() Message {
return msg
}
func (msg *SessionDisconnect) TypeID() int {
return 2
}
type SessionEnd struct {
message
Timestamp uint64
@ -2812,6 +2792,252 @@ func (msg *CreateIFrameDocument) TypeID() int {
return 70
}
type AdoptedSSReplaceURLBased struct {
message
SheetID uint64
Text string
BaseURL string
}
func (msg *AdoptedSSReplaceURLBased) Encode() []byte {
buf := make([]byte, 31+len(msg.Text)+len(msg.BaseURL))
buf[0] = 71
p := 1
p = WriteUint(msg.SheetID, buf, p)
p = WriteString(msg.Text, buf, p)
p = WriteString(msg.BaseURL, buf, p)
return buf[:p]
}
func (msg *AdoptedSSReplaceURLBased) EncodeWithIndex() []byte {
encoded := msg.Encode()
if IsIOSType(msg.TypeID()) {
return encoded
}
data := make([]byte, len(encoded)+8)
copy(data[8:], encoded[:])
binary.LittleEndian.PutUint64(data[0:], msg.Meta().Index)
return data
}
func (msg *AdoptedSSReplaceURLBased) Decode() Message {
return msg
}
func (msg *AdoptedSSReplaceURLBased) TypeID() int {
return 71
}
type AdoptedSSReplace struct {
message
SheetID uint64
Text string
}
func (msg *AdoptedSSReplace) Encode() []byte {
buf := make([]byte, 21+len(msg.Text))
buf[0] = 72
p := 1
p = WriteUint(msg.SheetID, buf, p)
p = WriteString(msg.Text, buf, p)
return buf[:p]
}
func (msg *AdoptedSSReplace) EncodeWithIndex() []byte {
encoded := msg.Encode()
if IsIOSType(msg.TypeID()) {
return encoded
}
data := make([]byte, len(encoded)+8)
copy(data[8:], encoded[:])
binary.LittleEndian.PutUint64(data[0:], msg.Meta().Index)
return data
}
func (msg *AdoptedSSReplace) Decode() Message {
return msg
}
func (msg *AdoptedSSReplace) TypeID() int {
return 72
}
type AdoptedSSInsertRuleURLBased struct {
message
SheetID uint64
Rule string
Index uint64
BaseURL string
}
func (msg *AdoptedSSInsertRuleURLBased) Encode() []byte {
buf := make([]byte, 41+len(msg.Rule)+len(msg.BaseURL))
buf[0] = 73
p := 1
p = WriteUint(msg.SheetID, buf, p)
p = WriteString(msg.Rule, buf, p)
p = WriteUint(msg.Index, buf, p)
p = WriteString(msg.BaseURL, buf, p)
return buf[:p]
}
func (msg *AdoptedSSInsertRuleURLBased) EncodeWithIndex() []byte {
encoded := msg.Encode()
if IsIOSType(msg.TypeID()) {
return encoded
}
data := make([]byte, len(encoded)+8)
copy(data[8:], encoded[:])
binary.LittleEndian.PutUint64(data[0:], msg.Meta().Index)
return data
}
func (msg *AdoptedSSInsertRuleURLBased) Decode() Message {
return msg
}
func (msg *AdoptedSSInsertRuleURLBased) TypeID() int {
return 73
}
type AdoptedSSInsertRule struct {
message
SheetID uint64
Rule string
Index uint64
}
func (msg *AdoptedSSInsertRule) Encode() []byte {
buf := make([]byte, 31+len(msg.Rule))
buf[0] = 74
p := 1
p = WriteUint(msg.SheetID, buf, p)
p = WriteString(msg.Rule, buf, p)
p = WriteUint(msg.Index, buf, p)
return buf[:p]
}
func (msg *AdoptedSSInsertRule) EncodeWithIndex() []byte {
encoded := msg.Encode()
if IsIOSType(msg.TypeID()) {
return encoded
}
data := make([]byte, len(encoded)+8)
copy(data[8:], encoded[:])
binary.LittleEndian.PutUint64(data[0:], msg.Meta().Index)
return data
}
func (msg *AdoptedSSInsertRule) Decode() Message {
return msg
}
func (msg *AdoptedSSInsertRule) TypeID() int {
return 74
}
type AdoptedSSDeleteRule struct {
message
SheetID uint64
Index uint64
}
func (msg *AdoptedSSDeleteRule) Encode() []byte {
buf := make([]byte, 21)
buf[0] = 75
p := 1
p = WriteUint(msg.SheetID, buf, p)
p = WriteUint(msg.Index, buf, p)
return buf[:p]
}
func (msg *AdoptedSSDeleteRule) EncodeWithIndex() []byte {
encoded := msg.Encode()
if IsIOSType(msg.TypeID()) {
return encoded
}
data := make([]byte, len(encoded)+8)
copy(data[8:], encoded[:])
binary.LittleEndian.PutUint64(data[0:], msg.Meta().Index)
return data
}
func (msg *AdoptedSSDeleteRule) Decode() Message {
return msg
}
func (msg *AdoptedSSDeleteRule) TypeID() int {
return 75
}
type AdoptedSSAddOwner struct {
message
SheetID uint64
ID uint64
}
func (msg *AdoptedSSAddOwner) Encode() []byte {
buf := make([]byte, 21)
buf[0] = 76
p := 1
p = WriteUint(msg.SheetID, buf, p)
p = WriteUint(msg.ID, buf, p)
return buf[:p]
}
func (msg *AdoptedSSAddOwner) EncodeWithIndex() []byte {
encoded := msg.Encode()
if IsIOSType(msg.TypeID()) {
return encoded
}
data := make([]byte, len(encoded)+8)
copy(data[8:], encoded[:])
binary.LittleEndian.PutUint64(data[0:], msg.Meta().Index)
return data
}
func (msg *AdoptedSSAddOwner) Decode() Message {
return msg
}
func (msg *AdoptedSSAddOwner) TypeID() int {
return 76
}
type AdoptedSSRemoveOwner struct {
message
SheetID uint64
ID uint64
}
func (msg *AdoptedSSRemoveOwner) Encode() []byte {
buf := make([]byte, 21)
buf[0] = 77
p := 1
p = WriteUint(msg.SheetID, buf, p)
p = WriteUint(msg.ID, buf, p)
return buf[:p]
}
func (msg *AdoptedSSRemoveOwner) EncodeWithIndex() []byte {
encoded := msg.Encode()
if IsIOSType(msg.TypeID()) {
return encoded
}
data := make([]byte, len(encoded)+8)
copy(data[8:], encoded[:])
binary.LittleEndian.PutUint64(data[0:], msg.Meta().Index)
return data
}
func (msg *AdoptedSSRemoveOwner) Decode() Message {
return msg
}
func (msg *AdoptedSSRemoveOwner) TypeID() int {
return 77
}
type IOSBatchMeta struct {
message
Timestamp uint64

View file

@ -117,15 +117,6 @@ func DecodeSessionStart(reader io.Reader) (Message, error) {
return msg, err
}
func DecodeSessionDisconnect(reader io.Reader) (Message, error) {
var err error = nil
msg := &SessionDisconnect{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
return msg, err
}
func DecodeSessionEnd(reader io.Reader) (Message, error) {
var err error = nil
msg := &SessionEnd{}
@ -1219,6 +1210,102 @@ func DecodeCreateIFrameDocument(reader io.Reader) (Message, error) {
return msg, err
}
func DecodeAdoptedSSReplaceURLBased(reader io.Reader) (Message, error) {
var err error = nil
msg := &AdoptedSSReplaceURLBased{}
if msg.SheetID, err = ReadUint(reader); err != nil {
return nil, err
}
if msg.Text, err = ReadString(reader); err != nil {
return nil, err
}
if msg.BaseURL, err = ReadString(reader); err != nil {
return nil, err
}
return msg, err
}
func DecodeAdoptedSSReplace(reader io.Reader) (Message, error) {
var err error = nil
msg := &AdoptedSSReplace{}
if msg.SheetID, err = ReadUint(reader); err != nil {
return nil, err
}
if msg.Text, err = ReadString(reader); err != nil {
return nil, err
}
return msg, err
}
func DecodeAdoptedSSInsertRuleURLBased(reader io.Reader) (Message, error) {
var err error = nil
msg := &AdoptedSSInsertRuleURLBased{}
if msg.SheetID, err = ReadUint(reader); err != nil {
return nil, err
}
if msg.Rule, err = ReadString(reader); err != nil {
return nil, err
}
if msg.Index, err = ReadUint(reader); err != nil {
return nil, err
}
if msg.BaseURL, err = ReadString(reader); err != nil {
return nil, err
}
return msg, err
}
func DecodeAdoptedSSInsertRule(reader io.Reader) (Message, error) {
var err error = nil
msg := &AdoptedSSInsertRule{}
if msg.SheetID, err = ReadUint(reader); err != nil {
return nil, err
}
if msg.Rule, err = ReadString(reader); err != nil {
return nil, err
}
if msg.Index, err = ReadUint(reader); err != nil {
return nil, err
}
return msg, err
}
func DecodeAdoptedSSDeleteRule(reader io.Reader) (Message, error) {
var err error = nil
msg := &AdoptedSSDeleteRule{}
if msg.SheetID, err = ReadUint(reader); err != nil {
return nil, err
}
if msg.Index, err = ReadUint(reader); err != nil {
return nil, err
}
return msg, err
}
func DecodeAdoptedSSAddOwner(reader io.Reader) (Message, error) {
var err error = nil
msg := &AdoptedSSAddOwner{}
if msg.SheetID, err = ReadUint(reader); err != nil {
return nil, err
}
if msg.ID, err = ReadUint(reader); err != nil {
return nil, err
}
return msg, err
}
func DecodeAdoptedSSRemoveOwner(reader io.Reader) (Message, error) {
var err error = nil
msg := &AdoptedSSRemoveOwner{}
if msg.SheetID, err = ReadUint(reader); err != nil {
return nil, err
}
if msg.ID, err = ReadUint(reader); err != nil {
return nil, err
}
return msg, err
}
func DecodeIOSBatchMeta(reader io.Reader) (Message, error) {
var err error = nil
msg := &IOSBatchMeta{}
@ -1639,9 +1726,6 @@ func ReadMessage(t uint64, reader io.Reader) (Message, error) {
case 1:
return DecodeSessionStart(reader)
case 2:
return DecodeSessionDisconnect(reader)
case 3:
return DecodeSessionEnd(reader)
@ -1834,6 +1918,27 @@ func ReadMessage(t uint64, reader io.Reader) (Message, error) {
case 70:
return DecodeCreateIFrameDocument(reader)
case 71:
return DecodeAdoptedSSReplaceURLBased(reader)
case 72:
return DecodeAdoptedSSReplace(reader)
case 73:
return DecodeAdoptedSSInsertRuleURLBased(reader)
case 74:
return DecodeAdoptedSSInsertRule(reader)
case 75:
return DecodeAdoptedSSDeleteRule(reader)
case 76:
return DecodeAdoptedSSAddOwner(reader)
case 77:
return DecodeAdoptedSSRemoveOwner(reader)
case 107:
return DecodeIOSBatchMeta(reader)

View file

@ -15,6 +15,25 @@ class BatchMeta(Message):
self.timestamp = timestamp
class BatchMetadata(Message):
__id__ = 81
def __init__(self, version, page_no, first_index, timestamp, location):
self.version = version
self.page_no = page_no
self.first_index = first_index
self.timestamp = timestamp
self.location = location
class PartitionedMessage(Message):
__id__ = 82
def __init__(self, part_no, part_total):
self.part_no = part_no
self.part_total = part_total
class Timestamp(Message):
__id__ = 0
@ -44,13 +63,6 @@ class SessionStart(Message):
self.user_id = user_id
class SessionDisconnect(Message):
__id__ = 2
def __init__(self, timestamp):
self.timestamp = timestamp
class SessionEnd(Message):
__id__ = 3
@ -637,13 +649,6 @@ class CustomIssue(Message):
self.payload = payload
class PageClose(Message):
__id__ = 65
def __init__(self, ):
class AssetCache(Message):
__id__ = 66
@ -679,6 +684,66 @@ class CreateIFrameDocument(Message):
self.id = id
class AdoptedSSReplaceURLBased(Message):
__id__ = 71
def __init__(self, sheet_id, text, base_url):
self.sheet_id = sheet_id
self.text = text
self.base_url = base_url
class AdoptedSSReplace(Message):
__id__ = 72
def __init__(self, sheet_id, text):
self.sheet_id = sheet_id
self.text = text
class AdoptedSSInsertRuleURLBased(Message):
__id__ = 73
def __init__(self, sheet_id, rule, index, base_url):
self.sheet_id = sheet_id
self.rule = rule
self.index = index
self.base_url = base_url
class AdoptedSSInsertRule(Message):
__id__ = 74
def __init__(self, sheet_id, rule, index):
self.sheet_id = sheet_id
self.rule = rule
self.index = index
class AdoptedSSDeleteRule(Message):
__id__ = 75
def __init__(self, sheet_id, index):
self.sheet_id = sheet_id
self.index = index
class AdoptedSSAddOwner(Message):
__id__ = 76
def __init__(self, sheet_id, id):
self.sheet_id = sheet_id
self.id = id
class AdoptedSSRemoveOwner(Message):
__id__ = 77
def __init__(self, sheet_id, id):
self.sheet_id = sheet_id
self.id = id
class IOSBatchMeta(Message):
__id__ = 107

View file

@ -26,6 +26,21 @@ class MessageCodec(Codec):
timestamp=self.read_int(reader)
)
if message_id == 81:
return BatchMetadata(
version=self.read_uint(reader),
page_no=self.read_uint(reader),
first_index=self.read_uint(reader),
timestamp=self.read_int(reader),
location=self.read_string(reader)
)
if message_id == 82:
return PartitionedMessage(
part_no=self.read_uint(reader),
part_total=self.read_uint(reader)
)
if message_id == 0:
return Timestamp(
timestamp=self.read_uint(reader)
@ -51,11 +66,6 @@ class MessageCodec(Codec):
user_id=self.read_string(reader)
)
if message_id == 2:
return SessionDisconnect(
timestamp=self.read_uint(reader)
)
if message_id == 3:
return SessionEnd(
timestamp=self.read_uint(reader)
@ -522,11 +532,6 @@ class MessageCodec(Codec):
payload=self.read_string(reader)
)
if message_id == 65:
return PageClose(
)
if message_id == 66:
return AssetCache(
url=self.read_string(reader)
@ -554,6 +559,52 @@ class MessageCodec(Codec):
id=self.read_uint(reader)
)
if message_id == 71:
return AdoptedSSReplaceURLBased(
sheet_id=self.read_uint(reader),
text=self.read_string(reader),
base_url=self.read_string(reader)
)
if message_id == 72:
return AdoptedSSReplace(
sheet_id=self.read_uint(reader),
text=self.read_string(reader)
)
if message_id == 73:
return AdoptedSSInsertRuleURLBased(
sheet_id=self.read_uint(reader),
rule=self.read_string(reader),
index=self.read_uint(reader),
base_url=self.read_string(reader)
)
if message_id == 74:
return AdoptedSSInsertRule(
sheet_id=self.read_uint(reader),
rule=self.read_string(reader),
index=self.read_uint(reader)
)
if message_id == 75:
return AdoptedSSDeleteRule(
sheet_id=self.read_uint(reader),
index=self.read_uint(reader)
)
if message_id == 76:
return AdoptedSSAddOwner(
sheet_id=self.read_uint(reader),
id=self.read_uint(reader)
)
if message_id == 77:
return AdoptedSSRemoveOwner(
sheet_id=self.read_uint(reader),
id=self.read_uint(reader)
)
if message_id == 107:
return IOSBatchMeta(
timestamp=self.read_uint(reader),

View file

@ -17,22 +17,6 @@ export interface Props {
function ChatWindow({ userId, incomeStream, localStream, endCall, isPrestart }: Props) {
const [localVideoEnabled, setLocalVideoEnabled] = useState(false)
const [remoteVideoEnabled, setRemoteVideoEnabled] = useState(false)
useEffect(() => {
if (!incomeStream || incomeStream.length === 0) { return }
const iid = setInterval(() => {
const settings = incomeStream.map(stream => stream.getVideoTracks()[0]?.getSettings()).filter(Boolean)
const isDummyVideoTrack = settings.length > 0 ? (settings.every(s => s.width === 2 || s.frameRate === 0 || s.frameRate === undefined)) : true
const shouldBeEnabled = !isDummyVideoTrack
if (shouldBeEnabled !== localVideoEnabled) {
setRemoteVideoEnabled(shouldBeEnabled)
}
}, 1000)
return () => clearInterval(iid)
}, [ incomeStream, localVideoEnabled ])
const minimize = !localVideoEnabled && !remoteVideoEnabled
return (
<Draggable handle=".handle" bounds="body">
@ -43,15 +27,18 @@ function ChatWindow({ userId, incomeStream, localStream, endCall, isPrestart }:
<div className="handle flex items-center p-2 cursor-move select-none border-b">
<div className={stl.headerTitle}>
<b>Talking to </b> {userId ? userId : 'Anonymous User'}
<br />
{incomeStream && incomeStream.length > 2 ? ' (+ other agents in the call)' : ''}
</div>
<Counter startTime={new Date().getTime() } className="text-sm ml-auto" />
</div>
<div className={cn(stl.videoWrapper, {'hidden' : minimize}, 'relative')}>
{!incomeStream && <div className={stl.noVideo}>Error obtaining incoming streams</div>}
{incomeStream && incomeStream.map(stream => <VideoContainer stream={ stream } />)}
<div className="absolute bottom-0 right-0 z-50">
<VideoContainer stream={ localStream ? localStream.stream : null } muted width={50} />
<div className={cn(stl.videoWrapper, 'relative')} style={{ minHeight: localVideoEnabled ? 52 : undefined}}>
{incomeStream
? incomeStream.map(stream => <React.Fragment key={stream.id}><VideoContainer stream={ stream } /></React.Fragment>) : (
<div className={stl.noVideo}>Error obtaining incoming streams</div>
)}
<div className={cn("absolute bottom-0 right-0 z-50", localVideoEnabled ? "" : "!hidden")}>
<VideoContainer stream={ localStream ? localStream.stream : null } muted height={50} />
</div>
</div>
<ChatControls videoEnabled={localVideoEnabled} setVideoEnabled={setLocalVideoEnabled} stream={localStream} endCall={endCall} isPrestart={isPrestart} />

View file

@ -2,7 +2,7 @@
background-color: white;
border: solid thin $gray-light;
border-radius: 3px;
position: fixed;
position: fixed;
width: 300px;
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);
}
@ -16,7 +16,7 @@
}
.videoWrapper {
height: 180px;
overflow: hidden;
max-height: 280px;
display: flex;
background-color: #000;
}
}

View file

@ -25,7 +25,7 @@ function onError(e) {
}
interface Props {
userId: String;
userId: string;
toggleChatWindow: (state) => void;
calling: CallingState;
annotating: boolean;
@ -69,7 +69,12 @@ function AssistActions({
}, [peerConnectionStatus]);
const addIncomeStream = (stream: MediaStream) => {
setIncomeStream(oldState => [...oldState, stream]);
setIncomeStream(oldState => {
if (!oldState.find(existingStream => existingStream.id === stream.id)) {
return [...oldState, stream]
}
return oldState
});
}
function call(agentIds?: string[]) {

View file

@ -6,18 +6,30 @@ interface Props {
width?: number
}
function VideoContainer({ stream, muted = false, width = 280 }: Props) {
function VideoContainer({ stream, muted = false, height = 280 }: Props) {
const ref = useRef<HTMLVideoElement>(null);
const [isEnabled, setEnabled] = React.useState(false);
useEffect(() => {
if (ref.current) {
ref.current.srcObject = stream;
}
}, [ ref.current, stream ])
}, [ ref.current, stream, stream.getVideoTracks()[0]?.getSettings().width ])
useEffect(() => {
if (!stream) { return }
const iid = setInterval(() => {
const settings = stream.getVideoTracks()[0]?.getSettings()
const isDummyVideoTrack = settings.width === 2 || settings.frameRate === 0
const shouldBeEnabled = !isDummyVideoTrack
isEnabled !== shouldBeEnabled ? setEnabled(shouldBeEnabled) : null;
}, 1000)
return () => clearInterval(iid)
}, [ stream, isEnabled ])
return (
<div>
<video autoPlay ref={ ref } muted={ muted } style={{ width: width }} />
<div className="flex-1" style={{ display: isEnabled ? undefined : 'none', border: "1px solid grey" }}>
<video autoPlay ref={ ref } muted={ muted } style={{ height: height }} />
</div>
)
}

View file

@ -54,7 +54,7 @@ export function getStatusText(status: ConnectionStatus): string {
return "Connected. Waiting for the data... (The tab might be inactive)"
}
}
export interface State {
calling: CallingState;
peerConnectionStatus: ConnectionStatus;
@ -77,11 +77,11 @@ const MAX_RECONNECTION_COUNT = 4;
export default class AssistManager {
private timeTravelJump = false;
private jumped = false;
constructor(private session: any, private md: MessageDistributor, private config: any) {}
private setStatus(status: ConnectionStatus) {
if (getState().peerConnectionStatus === ConnectionStatus.Disconnected &&
if (getState().peerConnectionStatus === ConnectionStatus.Disconnected &&
status !== ConnectionStatus.Connected) {
return
}
@ -109,7 +109,7 @@ export default class AssistManager {
if (document.hidden) {
this.socketCloseTimeout = setTimeout(() => {
const state = getState()
if (document.hidden &&
if (document.hidden &&
(state.calling === CallingState.NoCall && state.remoteControl === RemoteControlStatus.Enabled)) {
this.socket?.close()
}
@ -124,8 +124,16 @@ export default class AssistManager {
const jmr = new JSONRawMessageReader()
const reader = new MStreamReader(jmr)
let waitingForMessages = true
let showDisconnectTimeout: ReturnType<typeof setTimeout> | undefined
let disconnectTimeout: ReturnType<typeof setTimeout> | undefined
let inactiveTimeout: ReturnType<typeof setTimeout> | undefined
function clearDisconnectTimeout() {
disconnectTimeout && clearTimeout(disconnectTimeout)
disconnectTimeout = undefined
}
function clearInactiveTimeout() {
inactiveTimeout && clearTimeout(inactiveTimeout)
inactiveTimeout = undefined
}
const now = +new Date()
update({ assistStart: now })
@ -187,17 +195,17 @@ export default class AssistManager {
id === socket.id && this.toggleRemoteControl(false)
})
socket.on('SESSION_RECONNECTED', () => {
showDisconnectTimeout && clearTimeout(showDisconnectTimeout)
inactiveTimeout && clearTimeout(inactiveTimeout)
clearDisconnectTimeout()
clearInactiveTimeout()
this.setStatus(ConnectionStatus.Connected)
})
socket.on('UPDATE_SESSION', (data) => {
showDisconnectTimeout && clearTimeout(showDisconnectTimeout)
socket.on('UPDATE_SESSION', ({ active }) => {
clearDisconnectTimeout()
!inactiveTimeout && this.setStatus(ConnectionStatus.Connected)
if (typeof data.active === "boolean") {
if (data.active) {
inactiveTimeout && clearTimeout(inactiveTimeout)
if (typeof active === "boolean") {
clearInactiveTimeout()
if (active) {
this.setStatus(ConnectionStatus.Connected)
} else {
inactiveTimeout = setTimeout(() => this.setStatus(ConnectionStatus.Inactive), 5000)
@ -206,8 +214,8 @@ export default class AssistManager {
})
socket.on('SESSION_DISCONNECTED', e => {
waitingForMessages = true
showDisconnectTimeout && clearTimeout(showDisconnectTimeout)
showDisconnectTimeout = setTimeout(() => {
clearDisconnectTimeout()
disconnectTimeout = setTimeout(() => {
if (this.cleaned) { return }
this.setStatus(ConnectionStatus.Disconnected)
}, 30000)
@ -230,7 +238,7 @@ export default class AssistManager {
})
socket.on('call_end', this.onRemoteCallEnd)
document.addEventListener('visibilitychange', this.onVisChange)
document.addEventListener('visibilitychange', this.onVisChange)
})
}
@ -254,14 +262,14 @@ export default class AssistManager {
private onMouseClick = (e: MouseEvent): void => {
if (!this.socket) { return; }
if (getState().annotating) { return; } // ignore clicks while annotating
const data = this.md.getInternalViewportCoordinates(e)
// const el = this.md.getElementFromPoint(e); // requires requestiong node_id from domManager
const el = this.md.getElementFromInternalPoint(data)
if (el instanceof HTMLElement) {
el.focus()
el.oninput = e => {
if (el instanceof HTMLTextAreaElement
if (el instanceof HTMLTextAreaElement
|| el instanceof HTMLInputElement
) {
this.socket && this.socket.emit("input", el.value)
@ -346,7 +354,7 @@ export default class AssistManager {
console.log('getting call from', call.peer)
call.answer(this.callArgs.localStream.stream)
this.callConnection.push(call)
this.callArgs.localStream.onVideoTrack(vTrack => {
const sender = call.peerConnection.getSenders().find(s => s.track?.kind === "video")
if (!sender) {
@ -355,12 +363,12 @@ export default class AssistManager {
}
sender.replaceTrack(vTrack)
})
call.on('stream', stream => {
this.callArgs && this.callArgs.onStream(stream)
});
// call.peerConnection.addEventListener("track", e => console.log('newtrack',e.track))
call.on("close", this.onRemoteCallEnd)
call.on("error", (e) => {
console.error("PeerJS error (on call):", e)
@ -377,7 +385,7 @@ export default class AssistManager {
//call-reconnection connected
// if (['peer-unavailable', 'network', 'webrtc'].includes(e.type)) {
// this.setStatus(this.connectionAttempts++ < MAX_RECONNECTION_COUNT
// this.setStatus(this.connectionAttempts++ < MAX_RECONNECTION_COUNT
// ? ConnectionStatus.Connecting
// : ConnectionStatus.Disconnected);
// Reconnect...
@ -420,15 +428,15 @@ export default class AssistManager {
localStream: LocalStream,
onStream: (s: MediaStream)=>void,
onCallEnd: () => void,
onReject: () => void,
onReject: () => void,
onError?: ()=> void,
} | null = null
public setCallArgs(
localStream: LocalStream,
onStream: (s: MediaStream)=>void,
onCallEnd: () => void,
onReject: () => void,
localStream: LocalStream,
onStream: (s: MediaStream)=>void,
onCallEnd: () => void,
onReject: () => void,
onError?: ()=> void,
) {
this.callArgs = {
@ -451,7 +459,7 @@ export default class AssistManager {
}
}
/** Connecting to the other agents that are already
/** Connecting to the other agents that are already
* in the call with the user
*/
public addPeerCall(thirdPartyPeers: string[]) {

View file

@ -5,13 +5,13 @@ import type { Message, SetNodeScroll, CreateElementNode } from '../../messages';
import ListWalker from '../ListWalker';
import StylesManager, { rewriteNodeStyleSheet } from './StylesManager';
import { VElement, VText, VFragment, VDocument, VNode, VStyleElement } from './VirtualDOM';
import { VElement, VText, VShadowRoot, VDocument, VNode, VStyleElement } from './VirtualDOM';
import type { StyleElement } from './VirtualDOM';
type HTMLElementWithValue = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
const IGNORED_ATTRS = [ "autocomplete", "name" ];
const IGNORED_ATTRS = [ "autocomplete" ];
const ATTR_NAME_REGEXP = /([^\t\n\f \/>"'=]+)/; // regexp costs ~
@ -24,10 +24,32 @@ const ATTR_NAME_REGEXP = /([^\t\n\f \/>"'=]+)/; // regexp costs ~
// .replace(/\-webkit\-/g, "")
// }
function insertRule(sheet: CSSStyleSheet, msg: { rule: string, index: number }) {
try {
sheet.insertRule(msg.rule, msg.index)
} catch (e) {
logger.warn(e, msg)
try {
sheet.insertRule(msg.rule)
} catch (e) {
logger.warn("Cannot insert rule.", e, msg)
}
}
}
function deleteRule(sheet: CSSStyleSheet, msg: { index: number }) {
try {
sheet.deleteRule(msg.index)
} catch (e) {
logger.warn(e, msg)
}
}
export default class DOMManager extends ListWalker<Message> {
private vTexts: Map<number, VText> = new Map() // map vs object here?
private vElements: Map<number, VElement> = new Map()
private vRoots: Map<number, VFragment | VDocument> = new Map()
private vRoots: Map<number, VShadowRoot | VDocument> = new Map()
private styleSheets: Map<number, CSSStyleSheet> = new Map()
private upperBodyId: number = -1;
@ -42,6 +64,7 @@ export default class DOMManager extends ListWalker<Message> {
) {
super()
this.stylesManager = new StylesManager(screen)
logger.log(this.vElements)
}
append(m: Message): void {
@ -117,6 +140,7 @@ export default class DOMManager extends ListWalker<Message> {
let node: Node | undefined
let vn: VNode | undefined
let doc: Document | null
let styleSheet: CSSStyleSheet | undefined
switch (msg.tp) {
case "create_document":
doc = this.screen.document;
@ -134,7 +158,9 @@ export default class DOMManager extends ListWalker<Message> {
this.vElements = new Map([[0, vn]])
const vDoc = new VDocument(doc)
vDoc.insertChildAt(vn, 0)
this.vRoots = new Map([[-1, vDoc]]) // todo: start from 0 (sync logic with tracker)
this.vRoots = new Map([[0, vDoc]]) // watchout: id==0 for both Document and documentElement
// this is done for the AdoptedCSS logic
// todo: start from 0 (sync logic with tracker)
this.stylesManager.reset()
return
case "create_text_node":
@ -175,6 +201,10 @@ export default class DOMManager extends ListWalker<Message> {
let { name, value } = msg;
vn = this.vElements.get(msg.id)
if (!vn) { logger.error("Node not found", msg); return }
if (vn.node.tagName === "INPUT" && name === "name") {
// Otherwise binds local autocomplete values (maybe should ignore on the tracker level)
return
}
if (name === "href" && vn.node.tagName === "LINK") {
// @ts-ignore ?global ENV type // It've been done on backend (remove after testing in saas)
// if (value.startsWith(window.env.ASSETS_HOST || window.location.origin + '/assets')) {
@ -242,18 +272,7 @@ export default class DOMManager extends ListWalker<Message> {
logger.warn("Non-style node in CSS rules message (or sheet is null)", msg, vn);
return
}
vn.onStyleSheet(sheet => {
try {
sheet.insertRule(msg.rule, msg.index)
} catch (e) {
logger.warn(e, msg)
try {
sheet.insertRule(msg.rule)
} catch (e) {
logger.warn("Cannot insert rule.", e, msg)
}
}
})
vn.onStyleSheet(sheet => insertRule(sheet, msg))
return
case "css_delete_rule":
vn = this.vElements.get(msg.id)
@ -262,35 +281,27 @@ export default class DOMManager extends ListWalker<Message> {
logger.warn("Non-style node in CSS rules message (or sheet is null)", msg, vn);
return
}
vn.onStyleSheet(sheet => {
try {
sheet.deleteRule(msg.index)
} catch (e) {
logger.warn(e, msg)
}
})
vn.onStyleSheet(sheet => deleteRule(sheet, msg))
return
case "create_i_frame_document":
vn = this.vElements.get(msg.frameID)
if (!vn) { logger.error("Node not found", msg); return }
vn.enforceInsertion()
const host = vn.node
if (host instanceof HTMLIFrameElement) {
const vDoc = new VDocument()
this.vRoots.set(msg.id, vDoc)
host.onload = () => {
const doc = host.contentDocument
if (!doc) {
logger.warn("No iframe doc onload", msg, host)
return
}
vDoc.setDocument(doc)
vDoc.applyChanges()
}
const doc = host.contentDocument
if (!doc) {
logger.warn("No iframe doc onload", msg, host)
return
}
vDoc.setDocument(doc)
return;
} else if (host instanceof Element) { // shadow DOM
try {
const shadowRoot = host.attachShadow({ mode: 'open' })
vn = new VFragment(shadowRoot)
vn = new VShadowRoot(shadowRoot)
this.vRoots.set(msg.id, vn)
} catch(e) {
logger.warn("Can not attach shadow dom", e, msg)
@ -299,6 +310,60 @@ export default class DOMManager extends ListWalker<Message> {
logger.warn("Context message host is not Element", msg)
}
return
case "adopted_ss_insert_rule":
styleSheet = this.styleSheets.get(msg.sheetID)
if (!styleSheet) {
logger.warn("No stylesheet was created for ", msg)
return
}
insertRule(styleSheet, msg)
return
case "adopted_ss_delete_rule":
styleSheet = this.styleSheets.get(msg.sheetID)
if (!styleSheet) {
logger.warn("No stylesheet was created for ", msg)
return
}
deleteRule(styleSheet, msg)
return
case "adopted_ss_replace":
styleSheet = this.styleSheets.get(msg.sheetID)
if (!styleSheet) {
logger.warn("No stylesheet was created for ", msg)
return
}
// @ts-ignore
styleSheet.replaceSync(msg.text)
return
case "adopted_ss_add_owner":
vn = this.vRoots.get(msg.id)
if (!vn) { logger.error("Node not found", msg); return }
styleSheet = this.styleSheets.get(msg.sheetID)
if (!styleSheet) {
let context: typeof globalThis
const rootNode = vn.node
if (rootNode.nodeType === Node.DOCUMENT_NODE) {
context = (rootNode as Document).defaultView
} else {
context = (rootNode as ShadowRoot).ownerDocument.defaultView
}
styleSheet = new context.CSSStyleSheet()
this.styleSheets.set(msg.sheetID, styleSheet)
}
//@ts-ignore
vn.node.adoptedStyleSheets = [...vn.node.adoptedStyleSheets, styleSheet]
return
case "adopted_ss_remove_owner":
styleSheet = this.styleSheets.get(msg.sheetID)
if (!styleSheet) {
logger.warn("No stylesheet was created for ", msg)
return
}
vn = this.vRoots.get(msg.id)
if (!vn) { logger.error("Node not found", msg); return }
//@ts-ignore
vn.node.adoptedStyleSheets = [...vn.node.adoptedStyleSheets].filter(s => s !== styleSheet)
return
}
}

View file

@ -1,6 +1,6 @@
type VChild = VElement | VText
export type VNode = VDocument | VFragment | VElement | VText
export type VNode = VDocument | VShadowRoot | VElement | VText
abstract class VParent {
abstract node: Node | null
@ -67,6 +67,7 @@ export class VDocument extends VParent {
return
}
const child = this.children[0]
if (!child) { return }
child.applyChanges()
const htmlNode = child.node
if (htmlNode.parentNode !== this.node) {
@ -75,8 +76,8 @@ export class VDocument extends VParent {
}
}
export class VFragment extends VParent {
constructor(public readonly node: DocumentFragment) { super() }
export class VShadowRoot extends VParent {
constructor(public readonly node: ShadowRoot) { super() }
}
export class VElement extends VParent {
@ -122,31 +123,32 @@ export class VElement extends VParent {
type StyleSheetCallback = (s: CSSStyleSheet) => void
export type StyleElement = HTMLStyleElement | SVGStyleElement
export class VStyleElement extends VElement {
// private loaded = false
private loaded = false
private stylesheetCallbacks: StyleSheetCallback[] = []
constructor(public readonly node: StyleElement) {
super(node) // Is it compiled correctly or with 2 node assignments?
// node.onload = () => {
// const sheet = node.sheet
// if (sheet) {
// this.stylesheetCallbacks.forEach(cb => cb(sheet))
// } else {
// console.warn("Style onload: sheet is null")
// }
// this.loaded = true
// }
node.onload = () => {
const sheet = node.sheet
if (sheet) {
this.stylesheetCallbacks.forEach(cb => cb(sheet))
this.stylesheetCallbacks = []
} else {
console.warn("Style onload: sheet is null")
}
this.loaded = true
}
}
onStyleSheet(cb: StyleSheetCallback) {
// if (this.loaded) {
if (this.loaded) {
if (!this.node.sheet) {
console.warn("Style tag is loaded, but sheet is null")
return
}
cb(this.node.sheet)
// } else {
// this.stylesheetCallbacks.push(cb)
// }
} else {
this.stylesheetCallbacks.push(cb)
}
}
}

View file

@ -79,11 +79,11 @@ export default class WindowNodeCounter {
moveNode(id: number, parentId: number) {
if (!this.nodes[ id ]) {
console.error(`Wrong! Node with id ${ id } not found.`)
console.warn(`Node Counter: Node with id ${ id } not found.`)
return
}
if (!this.nodes[ parentId ]) {
console.error(`Wrong! Node with id ${ parentId } (parentId) not found.`)
console.warn(`Node Counter: Node with id ${ parentId } (parentId) not found.`)
return
}
this.nodes[ id ].moveNode(this.nodes[ parentId ])

View file

@ -1,18 +1,100 @@
import type { RawMessage } from './raw'
import type {
RawMessage,
RawSetNodeAttributeURLBased,
RawSetNodeAttribute,
RawSetCssDataURLBased,
RawSetCssData,
RawCssInsertRuleURLBased,
RawCssInsertRule,
RawAdoptedSsInsertRuleURLBased,
RawAdoptedSsInsertRule,
RawAdoptedSsReplaceURLBased,
RawAdoptedSsReplace,
} from './raw'
import type { TrackerMessage } from './tracker'
import translate from './tracker'
import { TP_MAP } from './tracker-legacy'
import { resolveURL, resolveCSS } from './urlResolve'
function legacyTranslate(msg: any): RawMessage | null {
const type = TP_MAP[msg._id as keyof typeof TP_MAP]
if (!type) {
return null
}
msg.tp = type
delete msg._id
return msg as RawMessage
}
// TODO: commonURLBased logic for feilds
const resolvers = {
"set_node_attribute_url_based": (msg: RawSetNodeAttributeURLBased): RawSetNodeAttribute =>
({
...msg,
value: msg.name === 'src' || msg.name === 'href'
? resolveURL(msg.baseURL, msg.value)
: (msg.name === 'style'
? resolveCSS(msg.baseURL, msg.value)
: msg.value
),
tp: "set_node_attribute",
}),
"set_css_data_url_based": (msg: RawSetCssDataURLBased): RawSetCssData =>
({
...msg,
data: resolveCSS(msg.baseURL, msg.data),
tp: "set_css_data",
}),
"css_insert_rule_url_based": (msg: RawCssInsertRuleURLBased): RawCssInsertRule =>
({
...msg,
rule: resolveCSS(msg.baseURL, msg.rule),
tp: "css_insert_rule",
}),
"adopted_ss_insert_rule_url_based": (msg: RawAdoptedSsInsertRuleURLBased): RawAdoptedSsInsertRule =>
({
...msg,
rule: resolveCSS(msg.baseURL, msg.rule),
tp: "adopted_ss_insert_rule",
}),
"adopted_ss_replace_url_based": (msg: RawAdoptedSsReplaceURLBased): RawAdoptedSsReplace =>
({
...msg,
text: resolveCSS(msg.baseURL, msg.text),
tp: "adopted_ss_replace"
})
} as const
type ResolvableType = keyof typeof resolvers
type ResolvableRawMessage = RawMessage & { tp: ResolvableType }
function isResolvable(msg: RawMessage): msg is ResolvableRawMessage {
//@ts-ignore
return resolvers[msg.tp] !== undefined
}
import { TP_MAP } from './raw'
export default class JSONRawMessageReader {
constructor(private messages: any[] = []){}
append(messages: any[]) {
constructor(private messages: TrackerMessage[] = []){}
append(messages: TrackerMessage[]) {
this.messages = this.messages.concat(messages)
}
readMessage(): RawMessage | null {
const msg = this.messages.shift()
let msg = this.messages.shift()
if (!msg) { return null }
msg.tp = TP_MAP[msg._id]
delete msg._id
return msg as RawMessage
const rawMsg = Array.isArray(msg)
? translate(msg)
: legacyTranslate(msg)
if (!rawMsg) {
return this.readMessage()
}
if (isResolvable(rawMsg)) {
//@ts-ignore ??? too complex typscript...
return resolvers[rawMsg.tp](rawMsg)
}
return rawMsg
}
}

View file

@ -1,68 +1,25 @@
import type { Message } from './message'
import type {
RawMessage,
RawSetNodeAttributeURLBased,
RawSetNodeAttribute,
RawSetCssDataURLBased,
RawSetCssData,
RawCssInsertRuleURLBased,
RawCssInsertRule,
} from './raw'
import type { RawMessage } from './raw'
import RawMessageReader from './RawMessageReader'
import { resolveURL, resolveCSS } from './urlResolve'
interface RawMessageReaderI {
readMessage(): RawMessage | null
}
const resolveMsg = {
"set_node_attribute_url_based": (msg: RawSetNodeAttributeURLBased): RawSetNodeAttribute =>
({
...msg,
value: msg.name === 'src' || msg.name === 'href'
? resolveURL(msg.baseURL, msg.value)
: (msg.name === 'style'
? resolveCSS(msg.baseURL, msg.value)
: msg.value
),
tp: "set_node_attribute",
}),
"set_css_data_url_based": (msg: RawSetCssDataURLBased): RawSetCssData =>
({
...msg,
data: resolveCSS(msg.baseURL, msg.data),
tp: "set_css_data",
}),
"css_insert_rule_url_based": (msg: RawCssInsertRuleURLBased): RawCssInsertRule =>
({
...msg,
rule: resolveCSS(msg.baseURL, msg.rule),
tp: "css_insert_rule",
})
}
export default class MStreamReader {
constructor(private readonly r: RawMessageReaderI = new RawMessageReader()){}
constructor(private readonly r: RawMessageReaderI = new RawMessageReader(), private startTs: number = 0){}
// append(buf: Uint8Array) {
// this.r.append(buf)
// }
private t0: number = 0
private t: number = 0
private idx: number = 0
readNext(): Message | null {
let msg = this.r.readMessage()
if (msg === null) { return null }
if (msg.tp === "timestamp" || msg.tp === "batch_meta") {
this.t0 = this.t0 || msg.timestamp
this.t = msg.timestamp - this.t0
if (msg.tp === "timestamp") {
this.startTs = this.startTs || msg.timestamp
this.t = msg.timestamp - this.startTs
return this.readNext()
}
// why typescript doesn't work here?
msg = (resolveMsg[msg.tp] || ((m:RawMessage)=>m))(msg)
return Object.assign(msg, {
time: this.t,
_index: this.idx++,

View file

@ -17,18 +17,6 @@ export default class RawMessageReader extends PrimitiveReader {
switch (tp) {
case 80: {
const pageNo = this.readUint(); if (pageNo === null) { return resetPointer() }
const firstIndex = this.readUint(); if (firstIndex === null) { return resetPointer() }
const timestamp = this.readInt(); if (timestamp === null) { return resetPointer() }
return {
tp: "batch_meta",
pageNo,
firstIndex,
timestamp,
};
}
case 0: {
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
return {
@ -37,14 +25,6 @@ export default class RawMessageReader extends PrimitiveReader {
};
}
case 2: {
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
return {
tp: "session_disconnect",
timestamp,
};
}
case 4: {
const url = this.readString(); if (url === null) { return resetPointer() }
const referrer = this.readString(); if (referrer === null) { return resetPointer() }
@ -187,16 +167,6 @@ export default class RawMessageReader extends PrimitiveReader {
};
}
case 17: {
const id = this.readUint(); if (id === null) { return resetPointer() }
const label = this.readString(); if (label === null) { return resetPointer() }
return {
tp: "set_input_target",
id,
label,
};
}
case 18: {
const id = this.readUint(); if (id === null) { return resetPointer() }
const value = this.readString(); if (value === null) { return resetPointer() }
@ -239,90 +209,6 @@ export default class RawMessageReader extends PrimitiveReader {
};
}
case 23: {
const requestStart = this.readUint(); if (requestStart === null) { return resetPointer() }
const responseStart = this.readUint(); if (responseStart === null) { return resetPointer() }
const responseEnd = this.readUint(); if (responseEnd === null) { return resetPointer() }
const domContentLoadedEventStart = this.readUint(); if (domContentLoadedEventStart === null) { return resetPointer() }
const domContentLoadedEventEnd = this.readUint(); if (domContentLoadedEventEnd === null) { return resetPointer() }
const loadEventStart = this.readUint(); if (loadEventStart === null) { return resetPointer() }
const loadEventEnd = this.readUint(); if (loadEventEnd === null) { return resetPointer() }
const firstPaint = this.readUint(); if (firstPaint === null) { return resetPointer() }
const firstContentfulPaint = this.readUint(); if (firstContentfulPaint === null) { return resetPointer() }
return {
tp: "page_load_timing",
requestStart,
responseStart,
responseEnd,
domContentLoadedEventStart,
domContentLoadedEventEnd,
loadEventStart,
loadEventEnd,
firstPaint,
firstContentfulPaint,
};
}
case 24: {
const speedIndex = this.readUint(); if (speedIndex === null) { return resetPointer() }
const visuallyComplete = this.readUint(); if (visuallyComplete === null) { return resetPointer() }
const timeToInteractive = this.readUint(); if (timeToInteractive === null) { return resetPointer() }
return {
tp: "page_render_timing",
speedIndex,
visuallyComplete,
timeToInteractive,
};
}
case 25: {
const name = this.readString(); if (name === null) { return resetPointer() }
const message = this.readString(); if (message === null) { return resetPointer() }
const payload = this.readString(); if (payload === null) { return resetPointer() }
return {
tp: "js_exception",
name,
message,
payload,
};
}
case 27: {
const name = this.readString(); if (name === null) { return resetPointer() }
const payload = this.readString(); if (payload === null) { return resetPointer() }
return {
tp: "raw_custom_event",
name,
payload,
};
}
case 28: {
const id = this.readString(); if (id === null) { return resetPointer() }
return {
tp: "user_id",
id,
};
}
case 29: {
const id = this.readString(); if (id === null) { return resetPointer() }
return {
tp: "user_anonymous_id",
id,
};
}
case 30: {
const key = this.readString(); if (key === null) { return resetPointer() }
const value = this.readString(); if (value === null) { return resetPointer() }
return {
tp: "metadata",
key,
value,
};
}
case 37: {
const id = this.readUint(); if (id === null) { return resetPointer() }
const rule = this.readString(); if (rule === null) { return resetPointer() }
@ -389,14 +275,6 @@ export default class RawMessageReader extends PrimitiveReader {
};
}
case 42: {
const type = this.readString(); if (type === null) { return resetPointer() }
return {
tp: "state_action",
type,
};
}
case 44: {
const action = this.readString(); if (action === null) { return resetPointer() }
const state = this.readString(); if (state === null) { return resetPointer() }
@ -469,28 +347,6 @@ export default class RawMessageReader extends PrimitiveReader {
};
}
case 53: {
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
const duration = this.readUint(); if (duration === null) { return resetPointer() }
const ttfb = this.readUint(); if (ttfb === null) { return resetPointer() }
const headerSize = this.readUint(); if (headerSize === null) { return resetPointer() }
const encodedBodySize = this.readUint(); if (encodedBodySize === null) { return resetPointer() }
const decodedBodySize = this.readUint(); if (decodedBodySize === null) { return resetPointer() }
const url = this.readString(); if (url === null) { return resetPointer() }
const initiator = this.readString(); if (initiator === null) { return resetPointer() }
return {
tp: "resource_timing",
timestamp,
duration,
ttfb,
headerSize,
encodedBodySize,
decodedBodySize,
url,
initiator,
};
}
case 54: {
const downlink = this.readUint(); if (downlink === null) { return resetPointer() }
const type = this.readString(); if (type === null) { return resetPointer() }
@ -555,34 +411,6 @@ export default class RawMessageReader extends PrimitiveReader {
};
}
case 63: {
const type = this.readString(); if (type === null) { return resetPointer() }
const value = this.readString(); if (value === null) { return resetPointer() }
return {
tp: "technical_info",
type,
value,
};
}
case 64: {
const name = this.readString(); if (name === null) { return resetPointer() }
const payload = this.readString(); if (payload === null) { return resetPointer() }
return {
tp: "custom_issue",
name,
payload,
};
}
case 65: {
return {
tp: "page_close",
};
}
case 67: {
const id = this.readUint(); if (id === null) { return resetPointer() }
const rule = this.readString(); if (rule === null) { return resetPointer() }
@ -621,6 +449,84 @@ export default class RawMessageReader extends PrimitiveReader {
};
}
case 71: {
const sheetID = this.readUint(); if (sheetID === null) { return resetPointer() }
const text = this.readString(); if (text === null) { return resetPointer() }
const baseURL = this.readString(); if (baseURL === null) { return resetPointer() }
return {
tp: "adopted_ss_replace_url_based",
sheetID,
text,
baseURL,
};
}
case 72: {
const sheetID = this.readUint(); if (sheetID === null) { return resetPointer() }
const text = this.readString(); if (text === null) { return resetPointer() }
return {
tp: "adopted_ss_replace",
sheetID,
text,
};
}
case 73: {
const sheetID = this.readUint(); if (sheetID === null) { return resetPointer() }
const rule = this.readString(); if (rule === null) { return resetPointer() }
const index = this.readUint(); if (index === null) { return resetPointer() }
const baseURL = this.readString(); if (baseURL === null) { return resetPointer() }
return {
tp: "adopted_ss_insert_rule_url_based",
sheetID,
rule,
index,
baseURL,
};
}
case 74: {
const sheetID = this.readUint(); if (sheetID === null) { return resetPointer() }
const rule = this.readString(); if (rule === null) { return resetPointer() }
const index = this.readUint(); if (index === null) { return resetPointer() }
return {
tp: "adopted_ss_insert_rule",
sheetID,
rule,
index,
};
}
case 75: {
const sheetID = this.readUint(); if (sheetID === null) { return resetPointer() }
const index = this.readUint(); if (index === null) { return resetPointer() }
return {
tp: "adopted_ss_delete_rule",
sheetID,
index,
};
}
case 76: {
const sheetID = this.readUint(); if (sheetID === null) { return resetPointer() }
const id = this.readUint(); if (id === null) { return resetPointer() }
return {
tp: "adopted_ss_add_owner",
sheetID,
id,
};
}
case 77: {
const sheetID = this.readUint(); if (sheetID === null) { return resetPointer() }
const id = this.readUint(); if (id === null) { return resetPointer() }
return {
tp: "adopted_ss_remove_owner",
sheetID,
id,
};
}
case 90: {
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
const projectID = this.readUint(); if (projectID === null) { return resetPointer() }

View file

@ -3,9 +3,7 @@
import type { Timed } from './timed'
import type { RawMessage } from './raw'
import type {
RawBatchMeta,
RawTimestamp,
RawSessionDisconnect,
RawSetPageLocation,
RawSetViewportSize,
RawSetViewportScroll,
@ -19,42 +17,36 @@ import type {
RawSetNodeData,
RawSetCssData,
RawSetNodeScroll,
RawSetInputTarget,
RawSetInputValue,
RawSetInputChecked,
RawMouseMove,
RawConsoleLog,
RawPageLoadTiming,
RawPageRenderTiming,
RawJsException,
RawRawCustomEvent,
RawUserID,
RawUserAnonymousID,
RawMetadata,
RawCssInsertRule,
RawCssDeleteRule,
RawFetch,
RawProfiler,
RawOTable,
RawStateAction,
RawRedux,
RawVuex,
RawMobX,
RawNgRx,
RawGraphQl,
RawPerformanceTrack,
RawResourceTiming,
RawConnectionInformation,
RawSetPageVisibility,
RawLongTask,
RawSetNodeAttributeURLBased,
RawSetCssDataURLBased,
RawTechnicalInfo,
RawCustomIssue,
RawPageClose,
RawCssInsertRuleURLBased,
RawMouseClick,
RawCreateIFrameDocument,
RawAdoptedSsReplaceURLBased,
RawAdoptedSsReplace,
RawAdoptedSsInsertRuleURLBased,
RawAdoptedSsInsertRule,
RawAdoptedSsDeleteRule,
RawAdoptedSsAddOwner,
RawAdoptedSsRemoveOwner,
RawIosSessionStart,
RawIosCustomEvent,
RawIosScreenChanges,
@ -67,12 +59,8 @@ import type {
export type Message = RawMessage & Timed
export type BatchMeta = RawBatchMeta & Timed
export type Timestamp = RawTimestamp & Timed
export type SessionDisconnect = RawSessionDisconnect & Timed
export type SetPageLocation = RawSetPageLocation & Timed
export type SetViewportSize = RawSetViewportSize & Timed
@ -99,8 +87,6 @@ export type SetCssData = RawSetCssData & Timed
export type SetNodeScroll = RawSetNodeScroll & Timed
export type SetInputTarget = RawSetInputTarget & Timed
export type SetInputValue = RawSetInputValue & Timed
export type SetInputChecked = RawSetInputChecked & Timed
@ -109,20 +95,6 @@ export type MouseMove = RawMouseMove & Timed
export type ConsoleLog = RawConsoleLog & Timed
export type PageLoadTiming = RawPageLoadTiming & Timed
export type PageRenderTiming = RawPageRenderTiming & Timed
export type JsException = RawJsException & Timed
export type RawCustomEvent = RawRawCustomEvent & Timed
export type UserID = RawUserID & Timed
export type UserAnonymousID = RawUserAnonymousID & Timed
export type Metadata = RawMetadata & Timed
export type CssInsertRule = RawCssInsertRule & Timed
export type CssDeleteRule = RawCssDeleteRule & Timed
@ -133,8 +105,6 @@ export type Profiler = RawProfiler & Timed
export type OTable = RawOTable & Timed
export type StateAction = RawStateAction & Timed
export type Redux = RawRedux & Timed
export type Vuex = RawVuex & Timed
@ -147,8 +117,6 @@ export type GraphQl = RawGraphQl & Timed
export type PerformanceTrack = RawPerformanceTrack & Timed
export type ResourceTiming = RawResourceTiming & Timed
export type ConnectionInformation = RawConnectionInformation & Timed
export type SetPageVisibility = RawSetPageVisibility & Timed
@ -159,18 +127,26 @@ export type SetNodeAttributeURLBased = RawSetNodeAttributeURLBased & Timed
export type SetCssDataURLBased = RawSetCssDataURLBased & Timed
export type TechnicalInfo = RawTechnicalInfo & Timed
export type CustomIssue = RawCustomIssue & Timed
export type PageClose = RawPageClose & Timed
export type CssInsertRuleURLBased = RawCssInsertRuleURLBased & Timed
export type MouseClick = RawMouseClick & Timed
export type CreateIFrameDocument = RawCreateIFrameDocument & Timed
export type AdoptedSsReplaceURLBased = RawAdoptedSsReplaceURLBased & Timed
export type AdoptedSsReplace = RawAdoptedSsReplace & Timed
export type AdoptedSsInsertRuleURLBased = RawAdoptedSsInsertRuleURLBased & Timed
export type AdoptedSsInsertRule = RawAdoptedSsInsertRule & Timed
export type AdoptedSsDeleteRule = RawAdoptedSsDeleteRule & Timed
export type AdoptedSsAddOwner = RawAdoptedSsAddOwner & Timed
export type AdoptedSsRemoveOwner = RawAdoptedSsRemoveOwner & Timed
export type IosSessionStart = RawIosSessionStart & Timed
export type IosCustomEvent = RawIosCustomEvent & Timed

View file

@ -1,85 +1,11 @@
// Auto-generated, do not edit
export const TP_MAP = {
80: "batch_meta",
0: "timestamp",
2: "session_disconnect",
4: "set_page_location",
5: "set_viewport_size",
6: "set_viewport_scroll",
7: "create_document",
8: "create_element_node",
9: "create_text_node",
10: "move_node",
11: "remove_node",
12: "set_node_attribute",
13: "remove_node_attribute",
14: "set_node_data",
15: "set_css_data",
16: "set_node_scroll",
17: "set_input_target",
18: "set_input_value",
19: "set_input_checked",
20: "mouse_move",
22: "console_log",
23: "page_load_timing",
24: "page_render_timing",
25: "js_exception",
27: "raw_custom_event",
28: "user_id",
29: "user_anonymous_id",
30: "metadata",
37: "css_insert_rule",
38: "css_delete_rule",
39: "fetch",
40: "profiler",
41: "o_table",
42: "state_action",
44: "redux",
45: "vuex",
46: "mob_x",
47: "ng_rx",
48: "graph_ql",
49: "performance_track",
53: "resource_timing",
54: "connection_information",
55: "set_page_visibility",
59: "long_task",
60: "set_node_attribute_url_based",
61: "set_css_data_url_based",
63: "technical_info",
64: "custom_issue",
65: "page_close",
67: "css_insert_rule_url_based",
69: "mouse_click",
70: "create_i_frame_document",
90: "ios_session_start",
93: "ios_custom_event",
96: "ios_screen_changes",
100: "ios_click_event",
102: "ios_performance_event",
103: "ios_log",
105: "ios_network_call",
}
export interface RawBatchMeta {
tp: "batch_meta",
pageNo: number,
firstIndex: number,
timestamp: number,
}
export interface RawTimestamp {
tp: "timestamp",
timestamp: number,
}
export interface RawSessionDisconnect {
tp: "session_disconnect",
timestamp: number,
}
export interface RawSetPageLocation {
tp: "set_page_location",
url: string,
@ -164,12 +90,6 @@ export interface RawSetNodeScroll {
y: number,
}
export interface RawSetInputTarget {
tp: "set_input_target",
id: number,
label: string,
}
export interface RawSetInputValue {
tp: "set_input_value",
id: number,
@ -195,55 +115,6 @@ export interface RawConsoleLog {
value: string,
}
export interface RawPageLoadTiming {
tp: "page_load_timing",
requestStart: number,
responseStart: number,
responseEnd: number,
domContentLoadedEventStart: number,
domContentLoadedEventEnd: number,
loadEventStart: number,
loadEventEnd: number,
firstPaint: number,
firstContentfulPaint: number,
}
export interface RawPageRenderTiming {
tp: "page_render_timing",
speedIndex: number,
visuallyComplete: number,
timeToInteractive: number,
}
export interface RawJsException {
tp: "js_exception",
name: string,
message: string,
payload: string,
}
export interface RawRawCustomEvent {
tp: "raw_custom_event",
name: string,
payload: string,
}
export interface RawUserID {
tp: "user_id",
id: string,
}
export interface RawUserAnonymousID {
tp: "user_anonymous_id",
id: string,
}
export interface RawMetadata {
tp: "metadata",
key: string,
value: string,
}
export interface RawCssInsertRule {
tp: "css_insert_rule",
id: number,
@ -282,11 +153,6 @@ export interface RawOTable {
value: string,
}
export interface RawStateAction {
tp: "state_action",
type: string,
}
export interface RawRedux {
tp: "redux",
action: string,
@ -329,18 +195,6 @@ export interface RawPerformanceTrack {
usedJSHeapSize: number,
}
export interface RawResourceTiming {
tp: "resource_timing",
timestamp: number,
duration: number,
ttfb: number,
headerSize: number,
encodedBodySize: number,
decodedBodySize: number,
url: string,
initiator: string,
}
export interface RawConnectionInformation {
tp: "connection_information",
downlink: number,
@ -378,23 +232,6 @@ export interface RawSetCssDataURLBased {
baseURL: string,
}
export interface RawTechnicalInfo {
tp: "technical_info",
type: string,
value: string,
}
export interface RawCustomIssue {
tp: "custom_issue",
name: string,
payload: string,
}
export interface RawPageClose {
tp: "page_close",
}
export interface RawCssInsertRuleURLBased {
tp: "css_insert_rule_url_based",
id: number,
@ -417,6 +254,52 @@ export interface RawCreateIFrameDocument {
id: number,
}
export interface RawAdoptedSsReplaceURLBased {
tp: "adopted_ss_replace_url_based",
sheetID: number,
text: string,
baseURL: string,
}
export interface RawAdoptedSsReplace {
tp: "adopted_ss_replace",
sheetID: number,
text: string,
}
export interface RawAdoptedSsInsertRuleURLBased {
tp: "adopted_ss_insert_rule_url_based",
sheetID: number,
rule: string,
index: number,
baseURL: string,
}
export interface RawAdoptedSsInsertRule {
tp: "adopted_ss_insert_rule",
sheetID: number,
rule: string,
index: number,
}
export interface RawAdoptedSsDeleteRule {
tp: "adopted_ss_delete_rule",
sheetID: number,
index: number,
}
export interface RawAdoptedSsAddOwner {
tp: "adopted_ss_add_owner",
sheetID: number,
id: number,
}
export interface RawAdoptedSsRemoveOwner {
tp: "adopted_ss_remove_owner",
sheetID: number,
id: number,
}
export interface RawIosSessionStart {
tp: "ios_session_start",
timestamp: number,
@ -488,4 +371,4 @@ export interface RawIosNetworkCall {
}
export type RawMessage = RawBatchMeta | RawTimestamp | RawSessionDisconnect | RawSetPageLocation | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputTarget | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawConsoleLog | RawPageLoadTiming | RawPageRenderTiming | RawJsException | RawRawCustomEvent | RawUserID | RawUserAnonymousID | RawMetadata | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawStateAction | RawRedux | RawVuex | RawMobX | RawNgRx | RawGraphQl | RawPerformanceTrack | RawResourceTiming | RawConnectionInformation | RawSetPageVisibility | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawTechnicalInfo | RawCustomIssue | RawPageClose | RawCssInsertRuleURLBased | RawMouseClick | RawCreateIFrameDocument | RawIosSessionStart | RawIosCustomEvent | RawIosScreenChanges | RawIosClickEvent | RawIosPerformanceEvent | RawIosLog | RawIosNetworkCall;
export type RawMessage = RawTimestamp | RawSetPageLocation | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawConsoleLog | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawRedux | RawVuex | RawMobX | RawNgRx | RawGraphQl | RawPerformanceTrack | RawConnectionInformation | RawSetPageVisibility | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawIosSessionStart | RawIosCustomEvent | RawIosScreenChanges | RawIosClickEvent | RawIosPerformanceEvent | RawIosLog | RawIosNetworkCall;

View file

@ -0,0 +1,72 @@
// @ts-nocheck
// Auto-generated, do not edit
export const TP_MAP = {
81: "batch_metadata",
82: "partitioned_message",
0: "timestamp",
4: "set_page_location",
5: "set_viewport_size",
6: "set_viewport_scroll",
7: "create_document",
8: "create_element_node",
9: "create_text_node",
10: "move_node",
11: "remove_node",
12: "set_node_attribute",
13: "remove_node_attribute",
14: "set_node_data",
15: "set_css_data",
16: "set_node_scroll",
17: "set_input_target",
18: "set_input_value",
19: "set_input_checked",
20: "mouse_move",
22: "console_log",
23: "page_load_timing",
24: "page_render_timing",
25: "js_exception",
27: "raw_custom_event",
28: "user_id",
29: "user_anonymous_id",
30: "metadata",
37: "css_insert_rule",
38: "css_delete_rule",
39: "fetch",
40: "profiler",
41: "o_table",
42: "state_action",
44: "redux",
45: "vuex",
46: "mob_x",
47: "ng_rx",
48: "graph_ql",
49: "performance_track",
53: "resource_timing",
54: "connection_information",
55: "set_page_visibility",
59: "long_task",
60: "set_node_attribute_url_based",
61: "set_css_data_url_based",
63: "technical_info",
64: "custom_issue",
67: "css_insert_rule_url_based",
69: "mouse_click",
70: "create_i_frame_document",
71: "adopted_ss_replace_url_based",
72: "adopted_ss_replace",
73: "adopted_ss_insert_rule_url_based",
74: "adopted_ss_insert_rule",
75: "adopted_ss_delete_rule",
76: "adopted_ss_add_owner",
77: "adopted_ss_remove_owner",
90: "ios_session_start",
93: "ios_custom_event",
96: "ios_screen_changes",
100: "ios_click_event",
102: "ios_performance_event",
103: "ios_log",
105: "ios_network_call",
} as const

View file

@ -0,0 +1,757 @@
// Auto-generated, do not edit
import type { RawMessage } from './raw'
type TrBatchMetadata = [
type: 81,
version: number,
pageNo: number,
firstIndex: number,
timestamp: number,
location: string,
]
type TrPartitionedMessage = [
type: 82,
partNo: number,
partTotal: number,
]
type TrTimestamp = [
type: 0,
timestamp: number,
]
type TrSetPageLocation = [
type: 4,
url: string,
referrer: string,
navigationStart: number,
]
type TrSetViewportSize = [
type: 5,
width: number,
height: number,
]
type TrSetViewportScroll = [
type: 6,
x: number,
y: number,
]
type TrCreateDocument = [
type: 7,
]
type TrCreateElementNode = [
type: 8,
id: number,
parentID: number,
index: number,
tag: string,
svg: boolean,
]
type TrCreateTextNode = [
type: 9,
id: number,
parentID: number,
index: number,
]
type TrMoveNode = [
type: 10,
id: number,
parentID: number,
index: number,
]
type TrRemoveNode = [
type: 11,
id: number,
]
type TrSetNodeAttribute = [
type: 12,
id: number,
name: string,
value: string,
]
type TrRemoveNodeAttribute = [
type: 13,
id: number,
name: string,
]
type TrSetNodeData = [
type: 14,
id: number,
data: string,
]
type TrSetNodeScroll = [
type: 16,
id: number,
x: number,
y: number,
]
type TrSetInputTarget = [
type: 17,
id: number,
label: string,
]
type TrSetInputValue = [
type: 18,
id: number,
value: string,
mask: number,
]
type TrSetInputChecked = [
type: 19,
id: number,
checked: boolean,
]
type TrMouseMove = [
type: 20,
x: number,
y: number,
]
type TrConsoleLog = [
type: 22,
level: string,
value: string,
]
type TrPageLoadTiming = [
type: 23,
requestStart: number,
responseStart: number,
responseEnd: number,
domContentLoadedEventStart: number,
domContentLoadedEventEnd: number,
loadEventStart: number,
loadEventEnd: number,
firstPaint: number,
firstContentfulPaint: number,
]
type TrPageRenderTiming = [
type: 24,
speedIndex: number,
visuallyComplete: number,
timeToInteractive: number,
]
type TrJSException = [
type: 25,
name: string,
message: string,
payload: string,
]
type TrRawCustomEvent = [
type: 27,
name: string,
payload: string,
]
type TrUserID = [
type: 28,
id: string,
]
type TrUserAnonymousID = [
type: 29,
id: string,
]
type TrMetadata = [
type: 30,
key: string,
value: string,
]
type TrCSSInsertRule = [
type: 37,
id: number,
rule: string,
index: number,
]
type TrCSSDeleteRule = [
type: 38,
id: number,
index: number,
]
type TrFetch = [
type: 39,
method: string,
url: string,
request: string,
response: string,
status: number,
timestamp: number,
duration: number,
]
type TrProfiler = [
type: 40,
name: string,
duration: number,
args: string,
result: string,
]
type TrOTable = [
type: 41,
key: string,
value: string,
]
type TrStateAction = [
type: 42,
type: string,
]
type TrRedux = [
type: 44,
action: string,
state: string,
duration: number,
]
type TrVuex = [
type: 45,
mutation: string,
state: string,
]
type TrMobX = [
type: 46,
type: string,
payload: string,
]
type TrNgRx = [
type: 47,
action: string,
state: string,
duration: number,
]
type TrGraphQL = [
type: 48,
operationKind: string,
operationName: string,
variables: string,
response: string,
]
type TrPerformanceTrack = [
type: 49,
frames: number,
ticks: number,
totalJSHeapSize: number,
usedJSHeapSize: number,
]
type TrResourceTiming = [
type: 53,
timestamp: number,
duration: number,
ttfb: number,
headerSize: number,
encodedBodySize: number,
decodedBodySize: number,
url: string,
initiator: string,
]
type TrConnectionInformation = [
type: 54,
downlink: number,
type: string,
]
type TrSetPageVisibility = [
type: 55,
hidden: boolean,
]
type TrLongTask = [
type: 59,
timestamp: number,
duration: number,
context: number,
containerType: number,
containerSrc: string,
containerId: string,
containerName: string,
]
type TrSetNodeAttributeURLBased = [
type: 60,
id: number,
name: string,
value: string,
baseURL: string,
]
type TrSetCSSDataURLBased = [
type: 61,
id: number,
data: string,
baseURL: string,
]
type TrTechnicalInfo = [
type: 63,
type: string,
value: string,
]
type TrCustomIssue = [
type: 64,
name: string,
payload: string,
]
type TrCSSInsertRuleURLBased = [
type: 67,
id: number,
rule: string,
index: number,
baseURL: string,
]
type TrMouseClick = [
type: 69,
id: number,
hesitationTime: number,
label: string,
selector: string,
]
type TrCreateIFrameDocument = [
type: 70,
frameID: number,
id: number,
]
type TrAdoptedSSReplaceURLBased = [
type: 71,
sheetID: number,
text: string,
baseURL: string,
]
type TrAdoptedSSInsertRuleURLBased = [
type: 73,
sheetID: number,
rule: string,
index: number,
baseURL: string,
]
type TrAdoptedSSDeleteRule = [
type: 75,
sheetID: number,
index: number,
]
type TrAdoptedSSAddOwner = [
type: 76,
sheetID: number,
id: number,
]
type TrAdoptedSSRemoveOwner = [
type: 77,
sheetID: number,
id: number,
]
export type TrackerMessage = TrBatchMetadata | TrPartitionedMessage | TrTimestamp | TrSetPageLocation | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrJSException | TrRawCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrRedux | TrVuex | TrMobX | TrNgRx | TrGraphQL | TrPerformanceTrack | TrResourceTiming | TrConnectionInformation | TrSetPageVisibility | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner
export default function translate(tMsg: TrackerMessage): RawMessage | null {
switch(tMsg[0]) {
case 0: {
return {
tp: "timestamp",
timestamp: tMsg[1],
}
}
case 4: {
return {
tp: "set_page_location",
url: tMsg[1],
referrer: tMsg[2],
navigationStart: tMsg[3],
}
}
case 5: {
return {
tp: "set_viewport_size",
width: tMsg[1],
height: tMsg[2],
}
}
case 6: {
return {
tp: "set_viewport_scroll",
x: tMsg[1],
y: tMsg[2],
}
}
case 7: {
return {
tp: "create_document",
}
}
case 8: {
return {
tp: "create_element_node",
id: tMsg[1],
parentID: tMsg[2],
index: tMsg[3],
tag: tMsg[4],
svg: tMsg[5],
}
}
case 9: {
return {
tp: "create_text_node",
id: tMsg[1],
parentID: tMsg[2],
index: tMsg[3],
}
}
case 10: {
return {
tp: "move_node",
id: tMsg[1],
parentID: tMsg[2],
index: tMsg[3],
}
}
case 11: {
return {
tp: "remove_node",
id: tMsg[1],
}
}
case 12: {
return {
tp: "set_node_attribute",
id: tMsg[1],
name: tMsg[2],
value: tMsg[3],
}
}
case 13: {
return {
tp: "remove_node_attribute",
id: tMsg[1],
name: tMsg[2],
}
}
case 14: {
return {
tp: "set_node_data",
id: tMsg[1],
data: tMsg[2],
}
}
case 16: {
return {
tp: "set_node_scroll",
id: tMsg[1],
x: tMsg[2],
y: tMsg[3],
}
}
case 18: {
return {
tp: "set_input_value",
id: tMsg[1],
value: tMsg[2],
mask: tMsg[3],
}
}
case 19: {
return {
tp: "set_input_checked",
id: tMsg[1],
checked: tMsg[2],
}
}
case 20: {
return {
tp: "mouse_move",
x: tMsg[1],
y: tMsg[2],
}
}
case 22: {
return {
tp: "console_log",
level: tMsg[1],
value: tMsg[2],
}
}
case 37: {
return {
tp: "css_insert_rule",
id: tMsg[1],
rule: tMsg[2],
index: tMsg[3],
}
}
case 38: {
return {
tp: "css_delete_rule",
id: tMsg[1],
index: tMsg[2],
}
}
case 39: {
return {
tp: "fetch",
method: tMsg[1],
url: tMsg[2],
request: tMsg[3],
response: tMsg[4],
status: tMsg[5],
timestamp: tMsg[6],
duration: tMsg[7],
}
}
case 40: {
return {
tp: "profiler",
name: tMsg[1],
duration: tMsg[2],
args: tMsg[3],
result: tMsg[4],
}
}
case 41: {
return {
tp: "o_table",
key: tMsg[1],
value: tMsg[2],
}
}
case 44: {
return {
tp: "redux",
action: tMsg[1],
state: tMsg[2],
duration: tMsg[3],
}
}
case 45: {
return {
tp: "vuex",
mutation: tMsg[1],
state: tMsg[2],
}
}
case 46: {
return {
tp: "mob_x",
type: tMsg[1],
payload: tMsg[2],
}
}
case 47: {
return {
tp: "ng_rx",
action: tMsg[1],
state: tMsg[2],
duration: tMsg[3],
}
}
case 48: {
return {
tp: "graph_ql",
operationKind: tMsg[1],
operationName: tMsg[2],
variables: tMsg[3],
response: tMsg[4],
}
}
case 49: {
return {
tp: "performance_track",
frames: tMsg[1],
ticks: tMsg[2],
totalJSHeapSize: tMsg[3],
usedJSHeapSize: tMsg[4],
}
}
case 54: {
return {
tp: "connection_information",
downlink: tMsg[1],
type: tMsg[2],
}
}
case 55: {
return {
tp: "set_page_visibility",
hidden: tMsg[1],
}
}
case 59: {
return {
tp: "long_task",
timestamp: tMsg[1],
duration: tMsg[2],
context: tMsg[3],
containerType: tMsg[4],
containerSrc: tMsg[5],
containerId: tMsg[6],
containerName: tMsg[7],
}
}
case 60: {
return {
tp: "set_node_attribute_url_based",
id: tMsg[1],
name: tMsg[2],
value: tMsg[3],
baseURL: tMsg[4],
}
}
case 61: {
return {
tp: "set_css_data_url_based",
id: tMsg[1],
data: tMsg[2],
baseURL: tMsg[3],
}
}
case 67: {
return {
tp: "css_insert_rule_url_based",
id: tMsg[1],
rule: tMsg[2],
index: tMsg[3],
baseURL: tMsg[4],
}
}
case 69: {
return {
tp: "mouse_click",
id: tMsg[1],
hesitationTime: tMsg[2],
label: tMsg[3],
selector: tMsg[4],
}
}
case 70: {
return {
tp: "create_i_frame_document",
frameID: tMsg[1],
id: tMsg[2],
}
}
case 71: {
return {
tp: "adopted_ss_replace_url_based",
sheetID: tMsg[1],
text: tMsg[2],
baseURL: tMsg[3],
}
}
case 73: {
return {
tp: "adopted_ss_insert_rule_url_based",
sheetID: tMsg[1],
rule: tMsg[2],
index: tMsg[3],
baseURL: tMsg[4],
}
}
case 75: {
return {
tp: "adopted_ss_delete_rule",
sheetID: tMsg[1],
index: tMsg[2],
}
}
case 76: {
return {
tp: "adopted_ss_add_owner",
sheetID: tMsg[1],
id: tMsg[2],
}
}
case 77: {
return {
tp: "adopted_ss_remove_owner",
sheetID: tMsg[1],
id: tMsg[2],
}
}
default:
return null
}
}

View file

@ -26,10 +26,9 @@ function cssUrlsIndex(css: string): Array<[number, number]> {
const e = s + m[1].length;
idxs.push([s, e])
}
return idxs.reverse();
return idxs.reverse()
}
function unquote(str: string): [string, string] {
str = str ? str.trim() : '';
if (str.length <= 2) {
return [str, ""]
}

View file

@ -8,9 +8,8 @@ ruby run.rb
```
In order generated .go file to fit the go formatting style:
In order format generated files run:
```sh
gofmt -w ../backend/pkg/messages/messages.go
sh format.sh
```
(Otherwise there will be changes in stage)

1
mobs/format.sh Normal file
View file

@ -0,0 +1 @@
gofmt -w ../backend/pkg/messages

View file

@ -1,12 +1,13 @@
# Special one for Batch Meta. Message id could define the version
# Special one for Batch Metadata. Message id could define the version
# Depricated since tracker 3.6.0 in favor of BatchMetadata
message 80, 'BatchMeta', :tracker => false, :replayer => false do
message 80, 'BatchMeta', :replayer => false, :tracker => false do
uint 'PageNo'
uint 'FirstIndex'
int 'Timestamp'
end
# since tracker 3.6.0
# since tracker 3.6.0 TODO: for webworker only
message 81, 'BatchMetadata', :replayer => false do
uint 'Version'
uint 'PageNo'
@ -43,10 +44,8 @@ message 1, 'SessionStart', :tracker => false, :replayer => false do
string 'UserCountry'
string 'UserID'
end
# Depricated (not used) since OpenReplay tracker 3.0.0
message 2, 'SessionDisconnect', :tracker => false do
uint 'Timestamp'
end
# message 2, 'CreateDocument', do
# end
message 3, 'SessionEnd', :tracker => false, :replayer => false do
uint 'Timestamp'
end
@ -63,6 +62,8 @@ message 6, 'SetViewportScroll' do
int 'X'
int 'Y'
end
# Depricated sinse tracker 3.6.0 in favor of CreateDocument(id=2)
# in order to use Document as a default root node instead of the documentElement
message 7, 'CreateDocument' do
end
message 8, 'CreateElementNode' do
@ -124,7 +125,7 @@ message 20, 'MouseMove' do
uint 'X'
uint 'Y'
end
# Depricated since OpenReplay 1.2.0
# Depricated since OpenReplay 1.2.0 (tracker version?)
message 21, 'MouseClickDepricated', :tracker => false, :replayer => false do
uint 'ID'
uint 'HesitationTime'
@ -371,14 +372,14 @@ message 59, 'LongTask' do
string 'ContainerId'
string 'ContainerName'
end
message 60, 'SetNodeAttributeURLBased', :replayer => false do
message 60, 'SetNodeAttributeURLBased' do
uint 'ID'
string 'Name'
string 'Value'
string 'BaseURL'
end
# Might replace SetCSSData (although BaseURL is useless after rewriting)
message 61, 'SetCSSDataURLBased', :replayer => false do
message 61, 'SetCSSDataURLBased' do
uint 'ID'
string 'Data'
string 'BaseURL'
@ -402,7 +403,7 @@ end
message 66, 'AssetCache', :replayer => false, :tracker => false do
string 'URL'
end
message 67, 'CSSInsertRuleURLBased', :replayer => false do
message 67, 'CSSInsertRuleURLBased' do
uint 'ID'
string 'Rule'
uint 'Index'
@ -419,4 +420,38 @@ end
message 70, 'CreateIFrameDocument' do
uint 'FrameID'
uint 'ID'
end
end
#Since 3.6.0 AdoptedStyleSheets
message 71, 'AdoptedSSReplaceURLBased' do
uint 'SheetID'
string 'Text'
string 'BaseURL'
end
message 72, 'AdoptedSSReplace', :tracker => false do
uint 'SheetID'
string 'Text'
end
message 73, 'AdoptedSSInsertRuleURLBased' do
uint 'SheetID'
string 'Rule'
uint 'Index'
string 'BaseURL'
end
message 74, 'AdoptedSSInsertRule', :tracker => false do
uint 'SheetID'
string 'Rule'
uint 'Index'
end
message 75, 'AdoptedSSDeleteRule' do
uint 'SheetID'
uint 'Index'
end
message 76, 'AdoptedSSAddOwner' do
uint 'SheetID'
uint 'ID'
end
message 77, 'AdoptedSSRemoveOwner' do
uint 'SheetID'
uint 'ID'
end

View file

@ -1,6 +1,8 @@
// Auto-generated, do not edit
package messages
import "encoding/binary"
const (
<% $messages.each do |msg| %>
Msg<%= msg.name %> = <%= msg.id %>

View file

@ -16,7 +16,7 @@ export default class RawMessageReader extends PrimitiveReader {
if (tp === null) { return resetPointer() }
switch (tp) {
<% $messages.select { |msg| msg.tracker || msg.replayer }.each do |msg| %>
<% $messages.select { |msg| msg.replayer }.each do |msg| %>
case <%= msg.id %>: {
<%= msg.attributes.map { |attr|
" const #{attr.name.camel_case} = this.read#{attr.type.to_s.pascal_case}(); if (#{attr.name.camel_case} === null) { return resetPointer() }" }.join "\n" %>

View file

@ -3,11 +3,11 @@
import type { Timed } from './timed'
import type { RawMessage } from './raw'
import type {
<%= $messages.select { |msg| msg.tracker || msg.replayer }.map { |msg| " Raw#{msg.name.snake_case.pascal_case}," }.join "\n" %>
<%= $messages.select { |msg| msg.replayer }.map { |msg| " Raw#{msg.name.snake_case.pascal_case}," }.join "\n" %>
} from './raw'
export type Message = RawMessage & Timed
<% $messages.select { |msg| msg.tracker || msg.replayer }.each do |msg| %>
<% $messages.select { |msg| msg.replayer }.each do |msg| %>
export type <%= msg.name.snake_case.pascal_case %> = Raw<%= msg.name.snake_case.pascal_case %> & Timed
<% end %>

View file

@ -1,14 +1,10 @@
// Auto-generated, do not edit
export const TP_MAP = {
<%= $messages.select { |msg| msg.tracker || msg.replayer }.map { |msg| " #{msg.id}: \"#{msg.name.snake_case}\"," }.join "\n" %>
}
<% $messages.select { |msg| msg.tracker || msg.replayer }.each do |msg| %>
<% $messages.select { |msg| msg.replayer }.each do |msg| %>
export interface Raw<%= msg.name.snake_case.pascal_case %> {
tp: "<%= msg.name.snake_case %>",
<%= msg.attributes.map { |attr| " #{attr.name.camel_case}: #{attr.type_js}," }.join "\n" %>
}
<% end %>
export type RawMessage = <%= $messages.select { |msg| msg.tracker || msg.replayer }.map { |msg| "Raw#{msg.name.snake_case.pascal_case}" }.join " | " %>;
export type RawMessage = <%= $messages.select { |msg| msg.replayer }.map { |msg| "Raw#{msg.name.snake_case.pascal_case}" }.join " | " %>;

View file

@ -0,0 +1,8 @@
// @ts-nocheck
// Auto-generated, do not edit
export const TP_MAP = {
<%= $messages.select { |msg| msg.tracker || msg.replayer }.map { |msg| " #{msg.id}: \"#{msg.name.snake_case}\"," }.join "\n" %>
} as const

View file

@ -0,0 +1,28 @@
// Auto-generated, do not edit
import type { RawMessage } from './raw'
<% $messages.select { |msg| msg.tracker }.each do |msg| %>
type Tr<%= msg.name %> = [
type: <%= msg.id %>,
<%= msg.attributes.map { |attr| "#{attr.name.camel_case}: #{attr.type_js}," }.join "\n " %>
]
<% end %>
export type TrackerMessage = <%= $messages.select { |msg| msg.tracker }.map { |msg| "Tr#{msg.name}" }.join " | " %>
export default function translate(tMsg: TrackerMessage): RawMessage | null {
switch(tMsg[0]) {
<% $messages.select { |msg| msg.replayer & msg.tracker }.each do |msg| %>
case <%= msg.id %>: {
return {
tp: "<%= msg.name.snake_case %>",
<%= msg.attributes.map.with_index { |attr, i| "#{attr.name.camel_case}: tMsg[#{i+1}]," }.join "\n " %>
}
}
<% end %>
default:
return null
}
}

View file

@ -60,11 +60,11 @@ export default class Assist {
private agents: Record<string, Agent> = {}
private readonly options: Options
constructor(
private readonly app: App,
options?: Partial<Options>,
private readonly app: App,
options?: Partial<Options>,
private readonly noSecureMode: boolean = false,
) {
this.options = Object.assign({
this.options = Object.assign({
session_calling_peer_key: '__openreplay_calling_peer',
session_control_peer_key: '__openreplay_control_peer',
config: null,
@ -91,12 +91,12 @@ export default class Assist {
const observer = titleNode && new MutationObserver(() => {
this.emit('UPDATE_SESSION', { pageTitle: document.title, })
})
app.attachStartCallback(() => {
app.attachStartCallback(() => {
if (this.assistDemandedRestart) { return }
this.onStart()
observer && observer.observe(titleNode, { subtree: true, characterData: true, childList: true, })
})
app.attachStopCallback(() => {
app.attachStopCallback(() => {
if (this.assistDemandedRestart) { return }
this.clean()
observer && observer.disconnect()

View file

@ -51,6 +51,11 @@ export enum Type {
CSSInsertRuleURLBased = 67,
MouseClick = 69,
CreateIFrameDocument = 70,
AdoptedSSReplaceURLBased = 71,
AdoptedSSInsertRuleURLBased = 73,
AdoptedSSDeleteRule = 75,
AdoptedSSAddOwner = 76,
AdoptedSSRemoveOwner = 77,
}
@ -400,6 +405,39 @@ export type CreateIFrameDocument = [
id: number,
]
export type AdoptedSSReplaceURLBased = [
type: Type.AdoptedSSReplaceURLBased,
sheetID: number,
text: string,
baseURL: string,
]
type Message = BatchMetadata | PartitionedMessage | Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | ConsoleLog | PageLoadTiming | PageRenderTiming | JSException | RawCustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | Redux | Vuex | MobX | NgRx | GraphQL | PerformanceTrack | ResourceTiming | ConnectionInformation | SetPageVisibility | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | CreateIFrameDocument
export type AdoptedSSInsertRuleURLBased = [
type: Type.AdoptedSSInsertRuleURLBased,
sheetID: number,
rule: string,
index: number,
baseURL: string,
]
export type AdoptedSSDeleteRule = [
type: Type.AdoptedSSDeleteRule,
sheetID: number,
index: number,
]
export type AdoptedSSAddOwner = [
type: Type.AdoptedSSAddOwner,
sheetID: number,
id: number,
]
export type AdoptedSSRemoveOwner = [
type: Type.AdoptedSSRemoveOwner,
sheetID: number,
id: number,
]
type Message = BatchMetadata | PartitionedMessage | Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | ConsoleLog | PageLoadTiming | PageRenderTiming | JSException | RawCustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | Redux | Vuex | MobX | NgRx | GraphQL | PerformanceTrack | ResourceTiming | ConnectionInformation | SetPageVisibility | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner
export default Message

View file

@ -10,7 +10,11 @@ export function isTextNode(node: Node): node is Text {
return node.nodeType === Node.TEXT_NODE
}
export function isRootNode(node: Node): boolean {
export function isDocument(node: Node): node is Document {
return node.nodeType === Node.DOCUMENT_NODE
}
export function isRootNode(node: Node): node is Document | DocumentFragment {
return node.nodeType === Node.DOCUMENT_NODE || node.nodeType === Node.DOCUMENT_FRAGMENT_NODE
}

View file

@ -1,6 +1,6 @@
import type Message from './messages.gen.js'
import { Timestamp, Metadata, UserID } from './messages.gen.js'
import { timestamp, deprecationWarn } from '../utils.js'
import { timestamp as now, deprecationWarn } from '../utils.js'
import Nodes from './nodes.js'
import Observer from './observer/top_observer.js'
import Sanitizer from './sanitizer.js'
@ -13,6 +13,7 @@ import { deviceMemory, jsHeapSizeLimit } from '../modules/performance.js'
import type { Options as ObserverOptions } from './observer/top_observer.js'
import type { Options as SanitizerOptions } from './sanitizer.js'
import type { Options as LoggerOptions } from './logger.js'
import type { Options as SessOptions } from './session.js'
import type { Options as WebworkerOptions, WorkerMessageData } from '../../common/interaction.js'
// TODO: Unify and clearly describe options logic
@ -20,6 +21,7 @@ export interface StartOptions {
userID?: string
metadata?: Record<string, string>
forceNew?: boolean
sessionHash?: string
}
interface OnStartInfo {
@ -49,9 +51,9 @@ enum ActivityState {
type AppOptions = {
revID: string
node_id: string
session_reset_key: string
session_token_key: string
session_pageno_key: string
session_reset_key: string
local_uuid_key: string
ingestPoint: string
resourceBaseHref: string | null // resourceHref?
@ -60,12 +62,13 @@ type AppOptions = {
__is_snippet: boolean
__debug_report_edp: string | null
__debug__?: LoggerOptions
localStorage: Storage
sessionStorage: Storage
localStorage: Storage | null
sessionStorage: Storage | null
// @deprecated
onStart?: StartCallback
} & WebworkerOptions
} & WebworkerOptions &
SessOptions
export type Options = AppOptions & ObserverOptions & SanitizerOptions
@ -83,7 +86,7 @@ export default class App {
readonly localStorage: Storage
readonly sessionStorage: Storage
private readonly messages: Array<Message> = []
private readonly observer: Observer
/* private */ readonly observer: Observer // non-privat for attachContextCallback
private readonly startCallbacks: Array<StartCallback> = []
private readonly stopCallbacks: Array<() => any> = []
private readonly commitCallbacks: Array<CommitCallback> = []
@ -92,11 +95,7 @@ export default class App {
private activityState: ActivityState = ActivityState.NotActive
private readonly version = 'TRACKER_VERSION' // TODO: version compatability check inside each plugin.
private readonly worker?: Worker
constructor(
projectKey: string,
sessionToken: string | null | undefined,
options: Partial<Options>,
) {
constructor(projectKey: string, sessionToken: string | undefined, options: Partial<Options>) {
// if (options.onStart !== undefined) {
// deprecationWarn("'onStart' option", "tracker.start().then(/* handle session info */)")
// } ?? maybe onStart is good
@ -129,7 +128,9 @@ export default class App {
this.ticker.attach(() => this.commit())
this.debug = new Logger(this.options.__debug__)
this.notify = new Logger(this.options.verbose ? LogLevel.Warnings : LogLevel.Silent)
this.session = new Session()
this.localStorage = this.options.localStorage || window.localStorage
this.sessionStorage = this.options.sessionStorage || window.sessionStorage
this.session = new Session(this, this.options)
this.session.attachUpdateCallback(({ userID, metadata }) => {
if (userID != null) {
// TODO: nullable userID
@ -139,11 +140,10 @@ export default class App {
Object.entries(metadata).forEach(([key, value]) => this.send(Metadata(key, value)))
}
})
this.localStorage = this.options.localStorage
this.sessionStorage = this.options.sessionStorage
// @depricated (use sessionHash on start instead)
if (sessionToken != null) {
this.sessionStorage.setItem(this.options.session_token_key, sessionToken)
this.session.applySessionHash(sessionToken)
}
try {
@ -206,7 +206,7 @@ export default class App {
}
private commit(): void {
if (this.worker && this.messages.length) {
this.messages.unshift(Timestamp(timestamp()))
this.messages.unshift(Timestamp(now()))
this.worker.postMessage(this.messages)
this.commitCallbacks.forEach((cb) => cb(this.messages))
this.messages.length = 0
@ -220,7 +220,7 @@ export default class App {
fn.apply(this, args)
} catch (e) {
app._debug('safe_fn_call', e)
// time: timestamp(),
// time: now(),
// name: e.name,
// message: e.message,
// stack: e.stack
@ -256,19 +256,24 @@ export default class App {
const reqVer = version.split(/[.-]/)
const ver = this.version.split(/[.-]/)
for (let i = 0; i < 3; i++) {
if (Number(ver[i]) < Number(reqVer[i]) || isNaN(Number(ver[i])) || isNaN(Number(reqVer[i]))) {
if (isNaN(Number(ver[i])) || isNaN(Number(reqVer[i]))) {
return false
}
if (Number(ver[i]) > Number(reqVer[i])) {
return true
}
if (Number(ver[i]) < Number(reqVer[i])) {
return false
}
}
return true
}
private getStartInfo() {
private getTrackerInfo() {
return {
userUUID: this.localStorage.getItem(this.options.local_uuid_key),
projectKey: this.projectKey,
revID: this.revID,
timestamp: timestamp(), // shouldn't it be set once?
trackerVersion: this.version,
isSnippet: this.options.__is_snippet,
}
@ -276,14 +281,11 @@ export default class App {
getSessionInfo() {
return {
...this.session.getInfo(),
...this.getStartInfo(),
...this.getTrackerInfo(),
}
}
getSessionToken(): string | undefined {
const token = this.sessionStorage.getItem(this.options.session_token_key)
if (token !== null) {
return token
}
return this.session.getSessionToken()
}
getSessionID(): string | undefined {
return this.session.getInfo().sessionID || undefined
@ -298,7 +300,7 @@ export default class App {
if (typeof this.options.resourceBaseHref === 'string') {
return this.options.resourceBaseHref
} else if (typeof this.options.resourceBaseHref === 'object') {
//switch between types
//TODO: switch between types
}
if (document.baseURI) {
return document.baseURI
@ -343,21 +345,16 @@ export default class App {
)
}
this.activityState = ActivityState.Starting
let pageNo = 0
const pageNoStr = this.sessionStorage.getItem(this.options.session_pageno_key)
if (pageNoStr != null) {
pageNo = parseInt(pageNoStr)
pageNo++
if (startOpts.sessionHash) {
this.session.applySessionHash(startOpts.sessionHash)
}
this.sessionStorage.setItem(this.options.session_pageno_key, pageNo.toString())
const startInfo = this.getStartInfo()
const timestamp = now()
const startWorkerMsg: WorkerMessageData = {
type: 'start',
pageNo,
pageNo: this.session.incPageNo(),
ingestPoint: this.options.ingestPoint,
timestamp: startInfo.timestamp,
timestamp,
url: document.URL,
connAttemptCount: this.options.connAttemptCount,
connAttemptGap: this.options.connAttemptGap,
@ -382,9 +379,10 @@ export default class App {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...startInfo,
...this.getTrackerInfo(),
timestamp,
userID: this.session.getInfo().userID,
token: this.sessionStorage.getItem(this.options.session_token_key),
token: this.session.getSessionToken(),
deviceMemory,
jsHeapSizeLimit,
reset: startOpts.forceNew || sReset !== null,
@ -407,17 +405,25 @@ export default class App {
if (!this.worker) {
return Promise.reject('no worker found after start request (this might not happen)')
}
const { token, userUUID, sessionID, beaconSizeLimit } = r
const {
token,
userUUID,
sessionID,
beaconSizeLimit,
startTimestamp, // real startTS, derived from sessionID
} = r
if (
typeof token !== 'string' ||
typeof userUUID !== 'string' ||
//typeof startTimestamp !== 'number' ||
//typeof sessionID !== 'string' ||
(typeof beaconSizeLimit !== 'number' && typeof beaconSizeLimit !== 'undefined')
) {
return Promise.reject(`Incorrect server response: ${JSON.stringify(r)}`)
}
this.sessionStorage.setItem(this.options.session_token_key, token)
this.session.setSessionToken(token)
this.localStorage.setItem(this.options.local_uuid_key, userUUID)
this.session.update({ sessionID }) // TODO: no no-explicit 'any'
this.session.update({ sessionID, timestamp: startTimestamp || timestamp }) // TODO: no no-explicit 'any'
const startWorkerMsg: WorkerMessageData = {
type: 'auth',
token,
@ -441,8 +447,8 @@ export default class App {
return SuccessfulStart(onStartInfo)
})
.catch((reason) => {
this.sessionStorage.removeItem(this.options.session_token_key)
this.stop()
this.session.reset()
if (reason === CANCELED) {
return UnsuccessfulStart(CANCELED)
}
@ -468,7 +474,7 @@ export default class App {
})
}
}
stop(calledFromAPI = false, restarting = false): void {
stop(stopWorker = true): void {
if (this.activityState !== ActivityState.NotActive) {
try {
this.sanitizer.clear()
@ -476,11 +482,8 @@ export default class App {
this.nodes.clear()
this.ticker.stop()
this.stopCallbacks.forEach((cb) => cb())
if (calledFromAPI) {
this.session.reset()
}
this.notify.log('OpenReplay tracking stopped.')
if (this.worker && !restarting) {
if (this.worker && stopWorker) {
this.worker.postMessage('stop')
}
} finally {
@ -489,7 +492,7 @@ export default class App {
}
}
restart() {
this.stop(false, true)
this.stop(false)
this.start({ forceNew: false })
}
}

View file

@ -646,3 +646,64 @@ export function CreateIFrameDocument(
]
}
export function AdoptedSSReplaceURLBased(
sheetID: number,
text: string,
baseURL: string,
): Messages.AdoptedSSReplaceURLBased {
return [
Messages.Type.AdoptedSSReplaceURLBased,
sheetID,
text,
baseURL,
]
}
export function AdoptedSSInsertRuleURLBased(
sheetID: number,
rule: string,
index: number,
baseURL: string,
): Messages.AdoptedSSInsertRuleURLBased {
return [
Messages.Type.AdoptedSSInsertRuleURLBased,
sheetID,
rule,
index,
baseURL,
]
}
export function AdoptedSSDeleteRule(
sheetID: number,
index: number,
): Messages.AdoptedSSDeleteRule {
return [
Messages.Type.AdoptedSSDeleteRule,
sheetID,
index,
]
}
export function AdoptedSSAddOwner(
sheetID: number,
id: number,
): Messages.AdoptedSSAddOwner {
return [
Messages.Type.AdoptedSSAddOwner,
sheetID,
id,
]
}
export function AdoptedSSRemoveOwner(
sheetID: number,
id: number,
): Messages.AdoptedSSRemoveOwner {
return [
Messages.Type.AdoptedSSRemoveOwner,
sheetID,
id,
]
}

View file

@ -8,9 +8,11 @@ export default class Nodes {
constructor(private readonly node_id: string) {}
// Attached once per Tracker instance
attachNodeCallback(nodeCallback: NodeCallback): void {
this.nodeCallbacks.push(nodeCallback)
}
// TODO: what is the difference with app.attachEventListener. can we use only one of those?
attachElementListener(type: string, node: Element, elementListener: EventListener): void {
const id = this.getID(node)
if (id === undefined) {

View file

@ -43,17 +43,10 @@ function isObservable(node: Node): boolean {
- use document as a 0-node in the upper context (should be updated in player at first)
*/
/*
Nikita:
- rn we only send unbind event for parent (all child nodes will be cut in the live replay anyways)
to prevent sending 1k+ unbinds for child nodes and making replay file bigger than it should be
*/
enum RecentsType {
New,
Removed,
Changed,
RemovedChild,
}
export default abstract class Observer {
@ -76,7 +69,10 @@ export default abstract class Observer {
}
if (type === 'childList') {
for (let i = 0; i < mutation.removedNodes.length; i++) {
this.bindTree(mutation.removedNodes[i], true)
// Should be the same as bindTree(mutation.removedNodes[i]), but logic needs to be be untied
if (isObservable(mutation.removedNodes[i])) {
this.bindNode(mutation.removedNodes[i])
}
}
for (let i = 0; i < mutation.addedNodes.length; i++) {
this.bindTree(mutation.addedNodes[i])
@ -183,16 +179,11 @@ export default abstract class Observer {
if (isNew) {
this.recents.set(id, RecentsType.New)
} else if (this.recents.get(id) !== RecentsType.New) {
// can we do just `else` here?
this.recents.set(id, RecentsType.Removed)
}
}
private unbindChildNode(node: Node): void {
const [id] = this.app.nodes.registerNode(node)
this.recents.set(id, RecentsType.RemovedChild)
}
private bindTree(node: Node, isChildUnbinding = false): void {
private bindTree(node: Node): void {
if (!isObservable(node)) {
return
}
@ -202,7 +193,7 @@ export default abstract class Observer {
NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT,
{
acceptNode: (node) =>
isIgnored(node) || (this.app.nodes.getID(node) !== undefined && !isChildUnbinding)
isIgnored(node) || this.app.nodes.getID(node) !== undefined
? NodeFilter.FILTER_REJECT
: NodeFilter.FILTER_ACCEPT,
},
@ -210,18 +201,33 @@ export default abstract class Observer {
false,
)
while (walker.nextNode()) {
if (isChildUnbinding) {
this.unbindChildNode(walker.currentNode)
} else {
this.bindNode(walker.currentNode)
}
this.bindNode(walker.currentNode)
}
}
private unbindNode(node: Node) {
private unbindTree(node: Node) {
const id = this.app.nodes.unregisterNode(node)
if (id !== undefined && this.recents.get(id) === RecentsType.Removed) {
// Sending RemoveNode only for parent to maintain
this.app.send(RemoveNode(id))
// Unregistering all the children in order to clear the memory
const walker = document.createTreeWalker(
node,
NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT,
{
acceptNode: (node) =>
isIgnored(node) || this.app.nodes.getID(node) === undefined
? NodeFilter.FILTER_REJECT
: NodeFilter.FILTER_ACCEPT,
},
// @ts-ignore
false,
)
while (walker.nextNode()) {
this.app.nodes.unregisterNode(walker.currentNode)
}
// MBTODO: count and send RemovedNodesCount (for the page crash detection in heuristics)
}
}
@ -239,17 +245,17 @@ export default abstract class Observer {
if (!hasTag(node, 'HTML') || !this.isTopContext) {
if (parent === null) {
// Sometimes one observation contains attribute mutations for the removimg node, which gets ignored here.
// That shouldn't affect the visual rendering ( should it? )
this.unbindNode(node)
// That shouldn't affect the visual rendering ( should it? maybe when transition applied? )
this.unbindTree(node)
return false
}
parentID = this.app.nodes.getID(parent)
if (parentID === undefined) {
this.unbindNode(node)
this.unbindTree(node)
return false
}
if (!this.commitNode(parentID)) {
this.unbindNode(node)
this.unbindTree(node)
return false
}
this.app.sanitizer.handleNode(id, parentID, node)
@ -345,7 +351,8 @@ export default abstract class Observer {
this.clear()
}
// ISSSUE
// ISSSUE (nodeToBinde should be the same as node. Look at the comment about 0-node at the beginning of the file.)
// TODO: use one observer instance for all iframes/shadowRoots (composition instiad of inheritance)
protected observeRoot(
node: Node,
beforeCommit: (id?: number) => unknown,

View file

@ -12,6 +12,18 @@ export interface Options {
captureIFrames: boolean
}
type ContextCallback = (context: Window & typeof globalThis) => void
// Le truc - for defining an absolute offset for (nested) iframe documents. (To track mouse movments)
type Offset = { top: number; left: number }
type PatchedDocument = Document & {
__openreplay__getOffset: () => Offset
}
function isPatchedDocument(doc: Document): doc is PatchedDocument {
// @ts-ignore
return typeof doc.__openreplay__getOffset === 'function'
}
const attachShadowNativeFn = IN_BROWSER ? Element.prototype.attachShadow : () => new ShadowRoot()
export default class TopObserver extends Observer {
@ -44,25 +56,53 @@ export default class TopObserver extends Observer {
})
}
private readonly contextCallbacks: Array<ContextCallback> = []
// Attached once per Tracker instance
attachContextCallback(cb: ContextCallback) {
this.contextCallbacks.push(cb)
}
// Le truc
getDocumentOffset(doc: Document): Offset {
if (isPatchedDocument(doc)) {
return doc.__openreplay__getOffset()
}
return { top: 0, left: 0 }
}
private iframeObservers: IFrameObserver[] = []
private handleIframe(iframe: HTMLIFrameElement): void {
let doc: Document | null = null
let win: Window | null = null
const handle = this.app.safe(() => {
const id = this.app.nodes.getID(iframe)
if (id === undefined) {
return
} //log
if (iframe.contentDocument === doc) {
return
} // How frequently can it happen?
doc = iframe.contentDocument
if (!doc || !iframe.contentWindow) {
//log
return
}
const observer = new IFrameObserver(this.app)
const currentWin = iframe.contentWindow
const currentDoc = iframe.contentDocument
if (currentDoc && currentDoc !== doc) {
const observer = new IFrameObserver(this.app)
this.iframeObservers.push(observer)
observer.observe(iframe)
doc = currentDoc
this.iframeObservers.push(observer)
observer.observe(iframe)
// Le truc
;(doc as PatchedDocument).__openreplay__getOffset = () => {
const { top, left } = this.getDocumentOffset(iframe.ownerDocument)
return {
top: iframe.offsetTop + top,
left: iframe.offsetLeft + left,
}
}
}
if (currentWin && currentWin !== win) {
//@ts-ignore https://github.com/microsoft/TypeScript/issues/41684
this.contextCallbacks.forEach((cb) => cb(currentWin))
win = currentWin
}
})
iframe.addEventListener('load', handle) // why app.attachEventListener not working?
handle()

View file

@ -1,15 +1,26 @@
import type App from './index.js'
interface SessionInfo {
sessionID: string | null
sessionID: string | undefined
metadata: Record<string, string>
userID: string | null
timestamp: number
}
type OnUpdateCallback = (i: Partial<SessionInfo>) => void
export type Options = {
session_token_key: string
session_pageno_key: string
}
export default class Session {
private metadata: Record<string, string> = {}
private userID: string | null = null
private sessionID: string | null = null
private sessionID: string | undefined
private readonly callbacks: OnUpdateCallback[] = []
private timestamp = 0
constructor(private readonly app: App, private options: Options) {}
attachUpdateCallback(cb: OnUpdateCallback) {
this.callbacks.push(cb)
@ -35,6 +46,9 @@ export default class Session {
if (newInfo.sessionID !== undefined) {
this.sessionID = newInfo.sessionID
}
if (newInfo.timestamp !== undefined) {
this.timestamp = newInfo.timestamp
}
this.handleUpdate(newInfo)
}
@ -47,17 +61,69 @@ export default class Session {
this.handleUpdate({ userID })
}
private getPageNumber(): number | undefined {
const pageNoStr = this.app.sessionStorage.getItem(this.options.session_pageno_key)
if (pageNoStr == null) {
return undefined
}
return parseInt(pageNoStr)
}
incPageNo(): number {
let pageNo = this.getPageNumber()
if (pageNo === undefined) {
pageNo = 0
} else {
pageNo++
}
this.app.sessionStorage.setItem(this.options.session_pageno_key, pageNo.toString())
return pageNo
}
getSessionToken(): string | undefined {
return this.app.sessionStorage.getItem(this.options.session_token_key) || undefined
}
setSessionToken(token: string): void {
this.app.sessionStorage.setItem(this.options.session_token_key, token)
}
applySessionHash(hash: string) {
const hashParts = decodeURI(hash).split('&')
let token = hash
let pageNoStr = '100500' // back-compat for sessionToken
if (hashParts.length == 2) {
;[token, pageNoStr] = hashParts
}
if (!pageNoStr || !token) {
return
}
this.app.sessionStorage.setItem(this.options.session_token_key, token)
this.app.sessionStorage.setItem(this.options.session_pageno_key, pageNoStr)
}
getSessionHash(): string | undefined {
const pageNo = this.getPageNumber()
const token = this.getSessionToken()
if (pageNo === undefined || token === undefined) {
return
}
return encodeURI(String(pageNo) + '&' + token)
}
getInfo(): SessionInfo {
return {
sessionID: this.sessionID,
metadata: this.metadata,
userID: this.userID,
timestamp: this.timestamp,
}
}
reset(): void {
this.app.sessionStorage.removeItem(this.options.session_token_key)
this.metadata = {}
this.userID = null
this.sessionID = null
this.sessionID = undefined
this.timestamp = 0
}
}

View file

@ -19,6 +19,7 @@ import Performance from './modules/performance.js'
import Scroll from './modules/scroll.js'
import Viewport from './modules/viewport.js'
import CSSRules from './modules/cssrules.js'
import AdoptedStyleSheets from './modules/adoptedStyleSheets.js'
import { IN_BROWSER, deprecationWarn, DOCS_HOST } from './utils.js'
import type { Options as AppOptions } from './app/index.js'
@ -69,10 +70,8 @@ function processOptions(obj: any): obj is Options {
obj.projectKey = obj.projectKey.toString()
}
}
if (typeof obj.sessionToken !== 'string' && obj.sessionToken != null) {
console.warn(
`OpenReplay: invalid options argument type. Please, check documentation on ${DOCS_HOST}${DOCS_SETUP}`,
)
if (obj.sessionToken != null) {
deprecationWarn('`sessionToken` option', '`sessionHash` start() option', '/')
}
return true
}
@ -113,6 +112,7 @@ export default class API {
if (app !== null) {
Viewport(app)
CSSRules(app)
AdoptedStyleSheets(app)
Connection(app)
Console(app, options)
Exception(app, options)
@ -182,11 +182,14 @@ export default class API {
// TODO: check argument type
return this.app.start(startOpts)
}
stop(): void {
stop(): string | undefined {
if (this.app === null) {
return
}
this.app.stop(true)
this.app.stop()
const sessionHash = this.app.session.getSessionHash()
this.app.session.reset()
return sessionHash
}
getSessionToken(): string | null | undefined {

View file

@ -0,0 +1,155 @@
import type App from '../app/index.js'
import {
TechnicalInfo,
AdoptedSSReplaceURLBased,
AdoptedSSInsertRuleURLBased,
AdoptedSSDeleteRule,
AdoptedSSAddOwner,
AdoptedSSRemoveOwner,
} from '../app/messages.gen.js'
import { isRootNode } from '../app/guards.js'
type StyleSheetOwner = (Document | ShadowRoot) & { adoptedStyleSheets: CSSStyleSheet[] }
function hasAdoptedSS(node: Node): node is StyleSheetOwner {
return (
isRootNode(node) &&
// @ts-ignore
!!node.adoptedStyleSheets
)
}
export default function (app: App | null) {
if (app === null) {
return
}
if (!hasAdoptedSS(document)) {
app.attachStartCallback(() => {
// MBTODO: pre-start sendQueue app
app.send(TechnicalInfo('no_adopted_stylesheets', ''))
})
return
}
let nextID = 0xf
const styleSheetIDMap: Map<CSSStyleSheet, number> = new Map()
const adoptedStyleSheetsOwnings: Map<number, number[]> = new Map()
const updateAdoptedStyleSheets = (root: StyleSheetOwner) => {
let nodeID = app.nodes.getID(root)
if (root === document) {
nodeID = 0 // main document doesn't have nodeID. ID count starts from the documentElement
}
if (!nodeID) {
return
}
let pastOwning = adoptedStyleSheetsOwnings.get(nodeID)
if (!pastOwning) {
pastOwning = []
}
const nowOwning: number[] = []
const styleSheets = root.adoptedStyleSheets
for (const s of styleSheets) {
let sheetID = styleSheetIDMap.get(s)
const init = !sheetID
if (!sheetID) {
sheetID = ++nextID
}
nowOwning.push(sheetID)
if (!pastOwning.includes(sheetID)) {
app.send(AdoptedSSAddOwner(sheetID, nodeID))
}
if (init) {
const rules = s.cssRules
for (let i = 0; i < rules.length; i++) {
app.send(AdoptedSSInsertRuleURLBased(sheetID, rules[i].cssText, i, app.getBaseHref()))
}
}
}
for (const sheetID of pastOwning) {
if (!nowOwning.includes(sheetID)) {
app.send(AdoptedSSRemoveOwner(sheetID, nodeID))
}
}
adoptedStyleSheetsOwnings.set(nodeID, nowOwning)
}
function patchAdoptedStyleSheets(
prototype: typeof Document.prototype | typeof ShadowRoot.prototype,
) {
const nativeAdoptedStyleSheetsDescriptor = Object.getOwnPropertyDescriptor(
prototype,
'adoptedStyleSheets',
)
if (nativeAdoptedStyleSheetsDescriptor) {
Object.defineProperty(prototype, 'adoptedStyleSheets', {
...nativeAdoptedStyleSheetsDescriptor,
set: function (this: StyleSheetOwner, value) {
// @ts-ignore
const retVal = nativeAdoptedStyleSheetsDescriptor.set.call(this, value)
updateAdoptedStyleSheets(this)
return retVal
},
})
}
}
const patchContext = (context: typeof globalThis): void => {
patchAdoptedStyleSheets(context.Document.prototype)
patchAdoptedStyleSheets(context.ShadowRoot.prototype)
//@ts-ignore TODO: configure ts (use necessary lib)
const { insertRule, deleteRule, replace, replaceSync } = context.CSSStyleSheet.prototype
//@ts-ignore
context.CSSStyleSheet.prototype.replace = function (text: string) {
return replace.call(this, text).then((sheet: CSSStyleSheet) => {
const sheetID = styleSheetIDMap.get(this)
if (sheetID) {
app.send(AdoptedSSReplaceURLBased(sheetID, text, app.getBaseHref()))
}
return sheet
})
}
//@ts-ignore
context.CSSStyleSheet.prototype.replaceSync = function (text: string) {
const sheetID = styleSheetIDMap.get(this)
if (sheetID) {
app.send(AdoptedSSReplaceURLBased(sheetID, text, app.getBaseHref()))
}
return replaceSync.call(this, text)
}
context.CSSStyleSheet.prototype.insertRule = function (rule: string, index = 0) {
const sheetID = styleSheetIDMap.get(this)
if (sheetID) {
app.send(AdoptedSSInsertRuleURLBased(sheetID, rule, index, app.getBaseHref()))
}
return insertRule.call(this, rule, index)
}
context.CSSStyleSheet.prototype.deleteRule = function (index: number) {
const sheetID = styleSheetIDMap.get(this)
if (sheetID) {
app.send(AdoptedSSDeleteRule(sheetID, index))
}
return deleteRule.call(this, index)
}
}
patchContext(window)
app.observer.attachContextCallback(patchContext)
app.attachStopCallback(() => {
styleSheetIDMap.clear()
adoptedStyleSheetsOwnings.clear()
})
// So far main Document is not triggered with nodeCallbacks
app.attachStartCallback(() => {
updateAdoptedStyleSheets(document as StyleSheetOwner)
})
app.nodes.attachNodeCallback((node: Node): void => {
if (hasAdoptedSS(node)) {
updateAdoptedStyleSheets(node)
}
})
}

View file

@ -122,7 +122,7 @@ export default function (app: App, opts: Partial<Options>): void {
const patchConsole = (console: Console) =>
options.consoleMethods!.forEach((method) => {
if (consoleMethods.indexOf(method) === -1) {
console.error(`OpenReplay: unsupported console method "${method}"`)
app.debug.error(`OpenReplay: unsupported console method "${method}"`)
return
}
const fn = (console as any)[method]
@ -134,23 +134,8 @@ export default function (app: App, opts: Partial<Options>): void {
sendConsoleLog(method, args)
}
})
patchConsole(window.console)
const patchContext = app.safe((context: typeof globalThis) => patchConsole(context.console))
app.nodes.attachNodeCallback(
app.safe((node) => {
if (hasTag(node, 'IFRAME')) {
// TODO: newContextCallback
let context = node.contentWindow
if (context) {
patchConsole((context as Window & typeof globalThis).console)
}
app.attachEventListener(node, 'load', () => {
if (node.contentWindow !== context) {
context = node.contentWindow
patchConsole((context as Window & typeof globalThis).console)
}
})
}
}),
)
patchContext(window)
app.observer.attachContextCallback(patchContext)
}

View file

@ -27,16 +27,20 @@ export default function (app: App | null) {
} // else error?
})
const { insertRule, deleteRule } = CSSStyleSheet.prototype
const patchContext = (context: typeof globalThis) => {
const { insertRule, deleteRule } = context.CSSStyleSheet.prototype
context.CSSStyleSheet.prototype.insertRule = function (rule: string, index = 0) {
processOperation(this, index, rule)
return insertRule.call(this, rule, index)
}
context.CSSStyleSheet.prototype.deleteRule = function (index: number) {
processOperation(this, index)
return deleteRule.call(this, index)
}
}
CSSStyleSheet.prototype.insertRule = function (rule: string, index = 0) {
processOperation(this, index, rule)
return insertRule.call(this, rule, index)
}
CSSStyleSheet.prototype.deleteRule = function (index: number) {
processOperation(this, index)
return deleteRule.call(this, index)
}
patchContext(window)
app.observer.attachContextCallback(patchContext)
app.nodes.attachNodeCallback((node: Node): void => {
if (!hasTag(node, 'STYLE') || !node.sheet) {

View file

@ -37,6 +37,7 @@ export function getExceptionMessage(error: Error, fallbackStack: Array<StackFram
export function getExceptionMessageFromEvent(
e: ErrorEvent | PromiseRejectionEvent,
context: typeof globalThis = window,
): Message | null {
if (e instanceof ErrorEvent) {
if (e.error instanceof Error) {
@ -49,7 +50,7 @@ export function getExceptionMessageFromEvent(
}
return JSException(name, message, JSON.stringify(getDefaultStack(e)))
}
} else if ('PromiseRejectionEvent' in window && e instanceof PromiseRejectionEvent) {
} else if ('PromiseRejectionEvent' in context && e instanceof context.PromiseRejectionEvent) {
if (e.reason instanceof Error) {
return getExceptionMessage(e.reason, [])
} else {
@ -72,17 +73,18 @@ export default function (app: App, opts: Partial<Options>): void {
},
opts,
)
if (options.captureExceptions) {
const handler = (e: ErrorEvent | PromiseRejectionEvent): void => {
const msg = getExceptionMessageFromEvent(e)
function patchContext(context: Window & typeof globalThis) {
function handler(e: ErrorEvent | PromiseRejectionEvent): void {
const msg = getExceptionMessageFromEvent(e, context)
if (msg != null) {
app.send(msg)
}
}
app.attachEventListener(window, 'unhandledrejection', (e: PromiseRejectionEvent): void =>
handler(e),
)
app.attachEventListener(window, 'error', (e: ErrorEvent): void => handler(e))
app.attachEventListener(context, 'unhandledrejection', handler)
app.attachEventListener(context, 'error', handler)
}
if (options.captureExceptions) {
app.observer.attachContextCallback(patchContext)
patchContext(window)
}
}

View file

@ -32,7 +32,24 @@ export default function (app: App): void {
}
}
const sendImgSrc = app.safe(function (this: HTMLImageElement): void {
const sendSrcset = function (id: number, img: HTMLImageElement): void {
const { srcset } = img
if (!srcset) {
return
}
const resolvedSrcset = srcset
.split(',')
.map((str) => resolveURL(str))
.join(',')
app.send(SetNodeAttribute(id, 'srcset', resolvedSrcset))
}
const sendSrc = function (id: number, img: HTMLImageElement): void {
const src = img.src
app.send(SetNodeAttributeURLBased(id, 'src', src, app.getBaseHref()))
}
const sendImgAttrs = app.safe(function (this: HTMLImageElement): void {
const id = app.nodes.getID(this)
if (id === undefined) {
return
@ -49,14 +66,8 @@ export default function (app: App): void {
} else if (resolvedSrc.length >= 1e5 || app.sanitizer.isMasked(id)) {
sendPlaceholder(id, this)
} else {
app.send(SetNodeAttribute(id, 'src', resolvedSrc))
if (srcset) {
const resolvedSrcset = srcset
.split(',')
.map((str) => resolveURL(str))
.join(',')
app.send(SetNodeAttribute(id, 'srcset', resolvedSrcset))
}
sendSrc(id, this)
sendSrcset(id, this)
}
})
@ -69,24 +80,26 @@ export default function (app: App): void {
return
}
if (mutation.attributeName === 'src') {
const src = target.src
app.send(SetNodeAttributeURLBased(id, 'src', src, app.getBaseHref()))
sendSrc(id, target)
}
if (mutation.attributeName === 'srcset') {
const srcset = target.srcset
app.send(SetNodeAttribute(id, 'srcset', srcset))
sendSrcset(id, target)
}
}
}
})
app.attachStopCallback(() => {
observer.disconnect()
})
app.nodes.attachNodeCallback((node: Node): void => {
if (!hasTag(node, 'IMG')) {
return
}
app.nodes.attachElementListener('error', node, sendImgSrc)
app.nodes.attachElementListener('load', node, sendImgSrc)
sendImgSrc.call(node)
app.nodes.attachElementListener('error', node, sendImgAttrs.bind(node))
app.nodes.attachElementListener('load', node, sendImgAttrs.bind(node))
sendImgAttrs.call(node)
observer.observe(node, { attributes: true, attributeFilter: ['src', 'srcset'] })
})
}

View file

@ -49,7 +49,7 @@ const labelElementFor: (element: TextEditableElement) => HTMLLabelElement | unde
}
const id = node.id
if (id) {
const labels = document.querySelectorAll('label[for="' + id + '"]')
const labels = node.ownerDocument.querySelectorAll('label[for="' + id + '"]')
if (labels !== null && labels.length === 1) {
return labels[0] as HTMLLabelElement
}

View file

@ -1,64 +0,0 @@
import type App from '../app/index.js'
import { LongTask } from '../app/messages.gen.js'
// https://w3c.github.io/performance-timeline/#the-performanceentry-interface
interface TaskAttributionTiming extends PerformanceEntry {
readonly containerType: string
readonly containerSrc: string
readonly containerId: string
readonly containerName: string
}
// https://www.w3.org/TR/longtasks/#performancelongtasktiming
interface PerformanceLongTaskTiming extends PerformanceEntry {
readonly attribution: ReadonlyArray<TaskAttributionTiming>
}
export default function (app: App): void {
if (!('PerformanceObserver' in window) || !('PerformanceLongTaskTiming' in window)) {
return
}
const contexts: string[] = [
'unknown',
'self',
'same-origin-ancestor',
'same-origin-descendant',
'same-origin',
'cross-origin-ancestor',
'cross-origin-descendant',
'cross-origin-unreachable',
'multiple-contexts',
]
const containerTypes: string[] = ['window', 'iframe', 'embed', 'object']
function longTask(entry: PerformanceLongTaskTiming): void {
let type = '',
src = '',
id = '',
name = ''
const container = entry.attribution[0]
if (container != null) {
type = container.containerType
name = container.containerName
id = container.containerId
src = container.containerSrc
}
app.send(
LongTask(
entry.startTime + performance.timing.navigationStart,
entry.duration,
Math.max(contexts.indexOf(entry.name), 0),
Math.max(containerTypes.indexOf(type), 0),
name,
id,
src,
),
)
}
const observer: PerformanceObserver = new PerformanceObserver((list) =>
list.getEntries().forEach(longTask),
)
observer.observe({ entryTypes: ['longtask'] })
}

View file

@ -1,10 +1,10 @@
import type App from '../app/index.js'
import { hasTag, isSVGElement } from '../app/guards.js'
import { hasTag, isSVGElement, isDocument } from '../app/guards.js'
import { normSpaces, hasOpenreplayAttribute, getLabelAttribute } from '../utils.js'
import { MouseMove, MouseClick } from '../app/messages.gen.js'
import { getInputLabel } from './input.js'
function _getSelector(target: Element): string {
function _getSelector(target: Element, document: Document): string {
let el: Element | null = target
let selector: string | null = null
do {
@ -37,18 +37,18 @@ function isClickable(element: Element): boolean {
element.getAttribute('role') === 'button'
)
//|| element.className.includes("btn")
// MBTODO: intersect addEventListener
// MBTODO: intersept addEventListener
}
//TODO: fix (typescript doesn't allow work when the guard is inside the function)
function getTarget(target: EventTarget | null): Element | null {
//TODO: fix (typescript is not sure about target variable after assignation of svg)
function getTarget(target: EventTarget | null, document: Document): Element | null {
if (target instanceof Element) {
return _getTarget(target)
return _getTarget(target, document)
}
return null
}
function _getTarget(target: Element): Element | null {
function _getTarget(target: Element, document: Document): Element | null {
let element: Element | null = target
while (element !== null && element !== document.documentElement) {
if (hasOpenreplayAttribute(element, 'masked')) {
@ -120,48 +120,58 @@ export default function (app: App): void {
}
}
const selectorMap: { [id: number]: string } = {}
function getSelector(id: number, target: Element): string {
return (selectorMap[id] = selectorMap[id] || _getSelector(target))
const patchDocument = (document: Document) => {
const selectorMap: { [id: number]: string } = {}
function getSelector(id: number, target: Element): string {
return (selectorMap[id] = selectorMap[id] || _getSelector(target, document))
}
app.attachEventListener(document.documentElement, 'mouseover', (e: MouseEvent): void => {
const target = getTarget(e.target, document)
if (target !== mouseTarget) {
mouseTarget = target
mouseTargetTime = performance.now()
}
})
app.attachEventListener(
document,
'mousemove',
(e: MouseEvent): void => {
const { top, left } = app.observer.getDocumentOffset(document)
mousePositionX = e.clientX + left
mousePositionY = e.clientY + top
mousePositionChanged = true
},
false,
)
app.attachEventListener(document, 'click', (e: MouseEvent): void => {
const target = getTarget(e.target, document)
if ((!e.clientX && !e.clientY) || target === null) {
return
}
const id = app.nodes.getID(target)
if (id !== undefined) {
sendMouseMove()
app.send(
MouseClick(
id,
mouseTarget === target ? Math.round(performance.now() - mouseTargetTime) : 0,
getTargetLabel(target),
getSelector(id, target),
),
true,
)
}
mouseTarget = null
})
}
app.attachEventListener(document.documentElement, 'mouseover', (e: MouseEvent): void => {
const target = getTarget(e.target)
if (target !== mouseTarget) {
mouseTarget = target
mouseTargetTime = performance.now()
app.nodes.attachNodeCallback((node) => {
if (isDocument(node)) {
patchDocument(node)
}
})
app.attachEventListener(
document,
'mousemove',
(e: MouseEvent): void => {
mousePositionX = e.clientX
mousePositionY = e.clientY
mousePositionChanged = true
},
false,
)
app.attachEventListener(document, 'click', (e: MouseEvent): void => {
const target = getTarget(e.target)
if ((!e.clientX && !e.clientY) || target === null) {
return
}
const id = app.nodes.getID(target)
if (id !== undefined) {
sendMouseMove()
app.send(
MouseClick(
id,
mouseTarget === target ? Math.round(performance.now() - mouseTargetTime) : 0,
getTargetLabel(target),
getSelector(id, target),
),
true,
)
}
mouseTarget = null
})
patchDocument(document)
app.ticker.attach(sendMouseMove, 10)
}

View file

@ -1,10 +1,16 @@
import type App from '../app/index.js'
import { SetViewportScroll, SetNodeScroll } from '../app/messages.gen.js'
import { isElementNode } from '../app/guards.js'
import { isElementNode, isRootNode } from '../app/guards.js'
export default function (app: App): void {
let documentScroll = false
const nodeScroll: Map<Element, [number, number]> = new Map()
const nodeScroll: Map<Node, [number, number]> = new Map()
function setNodeScroll(target: EventTarget | null) {
if (target instanceof Element) {
nodeScroll.set(target, [target.scrollLeft, target.scrollTop])
}
}
const sendSetViewportScroll = app.safe((): void =>
app.send(
@ -38,18 +44,21 @@ export default function (app: App): void {
app.nodes.attachNodeCallback((node, isStart) => {
if (isStart && isElementNode(node) && node.scrollLeft + node.scrollTop > 0) {
nodeScroll.set(node, [node.scrollLeft, node.scrollTop])
} else if (isRootNode(node)) {
// scroll is not-composed event (https://javascript.info/shadow-dom-events)
app.attachEventListener(node, 'scroll', (e: Event): void => {
setNodeScroll(e.target)
})
}
})
app.attachEventListener(window, 'scroll', (e: Event): void => {
app.attachEventListener(document, 'scroll', (e: Event): void => {
const target = e.target
if (target === document) {
documentScroll = true
return
}
if (target instanceof Element) {
nodeScroll.set(target, [target.scrollLeft, target.scrollTop])
}
setNodeScroll(target)
})
app.ticker.attach(

View file

@ -209,6 +209,26 @@ export default class MessageEncoder extends PrimitiveEncoder {
return this.uint(msg[1]) && this.uint(msg[2])
break
case Messages.Type.AdoptedSSReplaceURLBased:
return this.uint(msg[1]) && this.string(msg[2]) && this.string(msg[3])
break
case Messages.Type.AdoptedSSInsertRuleURLBased:
return this.uint(msg[1]) && this.string(msg[2]) && this.uint(msg[3]) && this.string(msg[4])
break
case Messages.Type.AdoptedSSDeleteRule:
return this.uint(msg[1]) && this.uint(msg[2])
break
case Messages.Type.AdoptedSSAddOwner:
return this.uint(msg[1]) && this.uint(msg[2])
break
case Messages.Type.AdoptedSSRemoveOwner:
return this.uint(msg[1]) && this.uint(msg[2])
break
}
}