feat(tracker): add beacon proxy and request body size (#1389)
* feat(tracker): add beacon proxy and body size * feat(ui): remove unused components * feat(ui): generate new messages, add body size to resource parser * feat(ui): fix tooltip text, fix size detection (ts safe) * feat(ui): cover resource with tests * feat(ui): enable test coverage for player, utils and mstore * fix(tracker): adjust test to support new message * fix(tracker): fix tracker version for back compat * feat(backend): added new column to network requests * fix(tracker): fix unit tests * fix(backend): fix msg gen * fix(tracker): ci fun * fix(tracker): changelog * fix(tracker): fix some test --------- Co-authored-by: Alexander Zavorotynskiy <zavorotynskiy@pm.me>
This commit is contained in:
parent
deba51833c
commit
c7e5145282
43 changed files with 2580 additions and 2600 deletions
|
|
@ -193,9 +193,9 @@ func (conn *BulkSet) initBulks() {
|
||||||
}
|
}
|
||||||
conn.webNetworkRequest, err = NewBulk(conn.c,
|
conn.webNetworkRequest, err = NewBulk(conn.c,
|
||||||
"events_common.requests",
|
"events_common.requests",
|
||||||
"(session_id, timestamp, seq_index, url, host, path, query, request_body, response_body, status_code, method, duration, success)",
|
"(session_id, timestamp, seq_index, url, host, path, query, request_body, response_body, status_code, method, duration, success, transfer_size)",
|
||||||
"($%d, $%d, $%d, LEFT($%d, 8000), LEFT($%d, 300), LEFT($%d, 2000), LEFT($%d, 8000), $%d, $%d, $%d::smallint, NULLIF($%d, '')::http_method, $%d, $%d)",
|
"($%d, $%d, $%d, LEFT($%d, 8000), LEFT($%d, 300), LEFT($%d, 2000), LEFT($%d, 8000), $%d, $%d, $%d::smallint, NULLIF($%d, '')::http_method, $%d, $%d, $%d)",
|
||||||
13, 200)
|
14, 200)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("can't create webNetworkRequest bulk: %s", err)
|
log.Fatalf("can't create webNetworkRequest bulk: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@ func (conn *Conn) InsertWebNetworkRequest(sess *sessions.Session, e *messages.Ne
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
conn.bulks.Get("webNetworkRequest").Append(sess.SessionID, e.Meta().Timestamp, truncSqIdx(e.Meta().Index), e.URL, host, path, query,
|
conn.bulks.Get("webNetworkRequest").Append(sess.SessionID, e.Meta().Timestamp, truncSqIdx(e.Meta().Index), e.URL, host, path, query,
|
||||||
request, response, e.Status, url.EnsureMethod(e.Method), e.Duration, e.Status < 400)
|
request, response, e.Status, url.EnsureMethod(e.Method), e.Duration, e.Status < 400, e.TransferredBodySize)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,4 @@ func IsIOSType(id int) bool {
|
||||||
|
|
||||||
func IsDOMType(id int) bool {
|
func IsDOMType(id int) bool {
|
||||||
return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 37 == id || 38 == id || 49 == id || 50 == id || 51 == id || 54 == id || 55 == id || 57 == id || 58 == id || 59 == id || 60 == id || 61 == id || 67 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 113 == id || 114 == id || 117 == id || 118 == id || 93 == id || 96 == id || 100 == id || 101 == id || 102 == id || 103 == id || 105 == id || 106 == 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 || 37 == id || 38 == id || 49 == id || 50 == id || 51 == id || 54 == id || 55 == id || 57 == id || 58 == id || 59 == id || 60 == id || 61 == id || 67 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 113 == id || 114 == id || 117 == id || 118 == id || 93 == id || 96 == id || 100 == id || 101 == id || 102 == id || 103 == id || 105 == id || 106 == id
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -113,7 +113,7 @@ var batches = map[string]string{
|
||||||
"inputs": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, label, event_type, duration, hesitation_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
"inputs": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, label, event_type, duration, hesitation_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
"errors": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, source, name, message, error_id, event_type, error_tags_keys, error_tags_values) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
"errors": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, source, name, message, error_id, event_type, error_tags_keys, error_tags_values) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
"performance": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, url, min_fps, avg_fps, max_fps, min_cpu, avg_cpu, max_cpu, min_total_js_heap_size, avg_total_js_heap_size, max_total_js_heap_size, min_used_js_heap_size, avg_used_js_heap_size, max_used_js_heap_size, event_type) VALUES (?, ?, ?, ?, SUBSTR(?, 1, 8000), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
"performance": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, url, min_fps, avg_fps, max_fps, min_cpu, avg_cpu, max_cpu, min_total_js_heap_size, avg_total_js_heap_size, max_total_js_heap_size, min_used_js_heap_size, avg_used_js_heap_size, max_used_js_heap_size, event_type) VALUES (?, ?, ?, ?, SUBSTR(?, 1, 8000), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
"requests": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, url, request_body, response_body, status, method, duration, success, event_type) VALUES (?, ?, ?, ?, SUBSTR(?, 1, 8000), ?, ?, ?, ?, ?, ?, ?)",
|
"requests": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, url, request_body, response_body, status, method, duration, success, event_type, transfer_size) VALUES (?, ?, ?, ?, SUBSTR(?, 1, 8000), ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
"custom": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, name, payload, event_type) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
"custom": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, name, payload, event_type) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
"graphql": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, name, request_body, response_body, event_type) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
"graphql": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, name, request_body, response_body, event_type) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
"issuesEvents": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, issue_id, issue_type, event_type, url) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
"issuesEvents": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, issue_id, issue_type, event_type, url) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
|
@ -474,6 +474,7 @@ func (c *connectorImpl) InsertRequest(session *sessions.Session, msg *messages.N
|
||||||
uint16(msg.Duration),
|
uint16(msg.Duration),
|
||||||
msg.Status < 400,
|
msg.Status < 400,
|
||||||
"REQUEST",
|
"REQUEST",
|
||||||
|
msg.TransferredBodySize,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
c.checkError("requests", err)
|
c.checkError("requests", err)
|
||||||
return fmt.Errorf("can't append to requests batch: %s", err)
|
return fmt.Errorf("can't append to requests batch: %s", err)
|
||||||
|
|
|
||||||
|
|
@ -185,7 +185,7 @@ class MouseMove(Message):
|
||||||
self.y = y
|
self.y = y
|
||||||
|
|
||||||
|
|
||||||
class NetworkRequest(Message):
|
class NetworkRequestDeprecated(Message):
|
||||||
__id__ = 21
|
__id__ = 21
|
||||||
|
|
||||||
def __init__(self, type, method, url, request, response, status, timestamp, duration):
|
def __init__(self, type, method, url, request, response, status, timestamp, duration):
|
||||||
|
|
@ -708,6 +708,21 @@ class PartitionedMessage(Message):
|
||||||
self.part_total = part_total
|
self.part_total = part_total
|
||||||
|
|
||||||
|
|
||||||
|
class NetworkRequest(Message):
|
||||||
|
__id__ = 83
|
||||||
|
|
||||||
|
def __init__(self, type, method, url, request, response, status, timestamp, duration, transferred_body_size):
|
||||||
|
self.type = type
|
||||||
|
self.method = method
|
||||||
|
self.url = url
|
||||||
|
self.request = request
|
||||||
|
self.response = response
|
||||||
|
self.status = status
|
||||||
|
self.timestamp = timestamp
|
||||||
|
self.duration = duration
|
||||||
|
self.transferred_body_size = transferred_body_size
|
||||||
|
|
||||||
|
|
||||||
class InputChange(Message):
|
class InputChange(Message):
|
||||||
__id__ = 112
|
__id__ = 112
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -268,7 +268,7 @@ cdef class MouseMove(PyMessage):
|
||||||
self.y = y
|
self.y = y
|
||||||
|
|
||||||
|
|
||||||
cdef class NetworkRequest(PyMessage):
|
cdef class NetworkRequestDeprecated(PyMessage):
|
||||||
cdef public int __id__
|
cdef public int __id__
|
||||||
cdef public str type
|
cdef public str type
|
||||||
cdef public str method
|
cdef public str method
|
||||||
|
|
@ -1044,6 +1044,31 @@ cdef class PartitionedMessage(PyMessage):
|
||||||
self.part_total = part_total
|
self.part_total = part_total
|
||||||
|
|
||||||
|
|
||||||
|
cdef class NetworkRequest(PyMessage):
|
||||||
|
cdef public int __id__
|
||||||
|
cdef public str type
|
||||||
|
cdef public str method
|
||||||
|
cdef public str url
|
||||||
|
cdef public str request
|
||||||
|
cdef public str response
|
||||||
|
cdef public unsigned long status
|
||||||
|
cdef public unsigned long timestamp
|
||||||
|
cdef public unsigned long duration
|
||||||
|
cdef public unsigned long transferred_body_size
|
||||||
|
|
||||||
|
def __init__(self, str type, str method, str url, str request, str response, unsigned long status, unsigned long timestamp, unsigned long duration, unsigned long transferred_body_size):
|
||||||
|
self.__id__ = 83
|
||||||
|
self.type = type
|
||||||
|
self.method = method
|
||||||
|
self.url = url
|
||||||
|
self.request = request
|
||||||
|
self.response = response
|
||||||
|
self.status = status
|
||||||
|
self.timestamp = timestamp
|
||||||
|
self.duration = duration
|
||||||
|
self.transferred_body_size = transferred_body_size
|
||||||
|
|
||||||
|
|
||||||
cdef class InputChange(PyMessage):
|
cdef class InputChange(PyMessage):
|
||||||
cdef public int __id__
|
cdef public int __id__
|
||||||
cdef public unsigned long id
|
cdef public unsigned long id
|
||||||
|
|
|
||||||
|
|
@ -233,7 +233,7 @@ class MessageCodec(Codec):
|
||||||
)
|
)
|
||||||
|
|
||||||
if message_id == 21:
|
if message_id == 21:
|
||||||
return NetworkRequest(
|
return NetworkRequestDeprecated(
|
||||||
type=self.read_string(reader),
|
type=self.read_string(reader),
|
||||||
method=self.read_string(reader),
|
method=self.read_string(reader),
|
||||||
url=self.read_string(reader),
|
url=self.read_string(reader),
|
||||||
|
|
@ -647,6 +647,19 @@ class MessageCodec(Codec):
|
||||||
part_total=self.read_uint(reader)
|
part_total=self.read_uint(reader)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if message_id == 83:
|
||||||
|
return NetworkRequest(
|
||||||
|
type=self.read_string(reader),
|
||||||
|
method=self.read_string(reader),
|
||||||
|
url=self.read_string(reader),
|
||||||
|
request=self.read_string(reader),
|
||||||
|
response=self.read_string(reader),
|
||||||
|
status=self.read_uint(reader),
|
||||||
|
timestamp=self.read_uint(reader),
|
||||||
|
duration=self.read_uint(reader),
|
||||||
|
transferred_body_size=self.read_uint(reader)
|
||||||
|
)
|
||||||
|
|
||||||
if message_id == 112:
|
if message_id == 112:
|
||||||
return InputChange(
|
return InputChange(
|
||||||
id=self.read_uint(reader),
|
id=self.read_uint(reader),
|
||||||
|
|
|
||||||
|
|
@ -331,7 +331,7 @@ cdef class MessageCodec:
|
||||||
)
|
)
|
||||||
|
|
||||||
if message_id == 21:
|
if message_id == 21:
|
||||||
return NetworkRequest(
|
return NetworkRequestDeprecated(
|
||||||
type=self.read_string(reader),
|
type=self.read_string(reader),
|
||||||
method=self.read_string(reader),
|
method=self.read_string(reader),
|
||||||
url=self.read_string(reader),
|
url=self.read_string(reader),
|
||||||
|
|
@ -745,6 +745,19 @@ cdef class MessageCodec:
|
||||||
part_total=self.read_uint(reader)
|
part_total=self.read_uint(reader)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if message_id == 83:
|
||||||
|
return NetworkRequest(
|
||||||
|
type=self.read_string(reader),
|
||||||
|
method=self.read_string(reader),
|
||||||
|
url=self.read_string(reader),
|
||||||
|
request=self.read_string(reader),
|
||||||
|
response=self.read_string(reader),
|
||||||
|
status=self.read_uint(reader),
|
||||||
|
timestamp=self.read_uint(reader),
|
||||||
|
duration=self.read_uint(reader),
|
||||||
|
transferred_body_size=self.read_uint(reader)
|
||||||
|
)
|
||||||
|
|
||||||
if message_id == 112:
|
if message_id == 112:
|
||||||
return InputChange(
|
return InputChange(
|
||||||
id=self.read_uint(reader),
|
id=self.read_uint(reader),
|
||||||
|
|
|
||||||
1
frontend/.gitignore
vendored
1
frontend/.gitignore
vendored
|
|
@ -19,3 +19,4 @@ cypress.env.json
|
||||||
**/__diff_output__/*
|
**/__diff_output__/*
|
||||||
*.diff.png
|
*.diff.png
|
||||||
cypress/videos/
|
cypress/videos/
|
||||||
|
coverage
|
||||||
|
|
@ -1,192 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { getRE } from 'App/utils';
|
|
||||||
import { Label, NoContent, Input, SlideModal, CloseButton, Icon } from 'UI';
|
|
||||||
import { connectPlayer, pause, jump } from 'Player';
|
|
||||||
// import Autoscroll from '../Autoscroll';
|
|
||||||
import BottomBlock from '../BottomBlock';
|
|
||||||
import TimeTable from '../TimeTable';
|
|
||||||
import FetchDetails from './FetchDetails';
|
|
||||||
import { renderName, renderDuration } from '../Network';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { setTimelinePointer } from 'Duck/sessions';
|
|
||||||
import { renderStart } from 'Components/Session_/Network/NetworkContent';
|
|
||||||
|
|
||||||
@connectPlayer((state) => ({
|
|
||||||
list: state.fetchList,
|
|
||||||
listNow: state.fetchListNow,
|
|
||||||
livePlay: state.livePlay,
|
|
||||||
}))
|
|
||||||
@connect(
|
|
||||||
(state) => ({
|
|
||||||
timelinePointer: state.getIn(['sessions', 'timelinePointer']),
|
|
||||||
}),
|
|
||||||
{ setTimelinePointer }
|
|
||||||
)
|
|
||||||
export default class Fetch extends React.PureComponent {
|
|
||||||
state = {
|
|
||||||
filter: '',
|
|
||||||
filteredList: this.props.list,
|
|
||||||
current: null,
|
|
||||||
currentIndex: 0,
|
|
||||||
showFetchDetails: false,
|
|
||||||
hasNextError: false,
|
|
||||||
hasPreviousError: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
onFilterChange = (e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
const { list } = this.props;
|
|
||||||
const filterRE = getRE(value, 'i');
|
|
||||||
const filtered = list.filter(
|
|
||||||
(r) =>
|
|
||||||
filterRE.test(r.name) ||
|
|
||||||
filterRE.test(r.url) ||
|
|
||||||
filterRE.test(r.method) ||
|
|
||||||
filterRE.test(r.status)
|
|
||||||
);
|
|
||||||
this.setState({ filter: value, filteredList: value ? filtered : list, currentIndex: 0 });
|
|
||||||
};
|
|
||||||
|
|
||||||
setCurrent = (item, index) => {
|
|
||||||
if (!this.props.livePlay) {
|
|
||||||
pause();
|
|
||||||
jump(item.time);
|
|
||||||
}
|
|
||||||
this.setState({ current: item, currentIndex: index });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRowClick = (item, index) => {
|
|
||||||
if (!this.props.livePlay) {
|
|
||||||
pause();
|
|
||||||
}
|
|
||||||
this.setState({ current: item, currentIndex: index, showFetchDetails: true });
|
|
||||||
this.props.setTimelinePointer(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
closeModal = () => this.setState({ current: null, showFetchDetails: false });
|
|
||||||
|
|
||||||
nextClickHander = () => {
|
|
||||||
// const { list } = this.props;
|
|
||||||
const { currentIndex, filteredList } = this.state;
|
|
||||||
|
|
||||||
if (currentIndex === filteredList.length - 1) return;
|
|
||||||
const newIndex = currentIndex + 1;
|
|
||||||
this.setCurrent(filteredList[newIndex], newIndex);
|
|
||||||
this.setState({ showFetchDetails: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
prevClickHander = () => {
|
|
||||||
// const { list } = this.props;
|
|
||||||
const { currentIndex, filteredList } = this.state;
|
|
||||||
|
|
||||||
if (currentIndex === 0) return;
|
|
||||||
const newIndex = currentIndex - 1;
|
|
||||||
this.setCurrent(filteredList[newIndex], newIndex);
|
|
||||||
this.setState({ showFetchDetails: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { listNow } = this.props;
|
|
||||||
const { current, currentIndex, showFetchDetails, filteredList } = this.state;
|
|
||||||
// const hasErrors = filteredList.some((r) => r.status >= 400);
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<SlideModal
|
|
||||||
right
|
|
||||||
size="middle"
|
|
||||||
title={
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<h1>Fetch Request</h1>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="mr-2 color-gray-medium uppercase text-base">Status</span>
|
|
||||||
<Label
|
|
||||||
data-red={current && current.status >= 400}
|
|
||||||
data-green={current && current.status < 400}
|
|
||||||
>
|
|
||||||
<div className="uppercase w-16 justify-center code-font text-lg">
|
|
||||||
{current && current.status}
|
|
||||||
</div>
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<CloseButton onClick={this.closeModal} size="18" className="ml-2" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
isDisplayed={current != null && showFetchDetails}
|
|
||||||
content={
|
|
||||||
current &&
|
|
||||||
showFetchDetails && (
|
|
||||||
<FetchDetails
|
|
||||||
resource={current}
|
|
||||||
nextClick={this.nextClickHander}
|
|
||||||
prevClick={this.prevClickHander}
|
|
||||||
first={currentIndex === 0}
|
|
||||||
last={currentIndex === filteredList.length - 1}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onClose={this.closeModal}
|
|
||||||
/>
|
|
||||||
<BottomBlock>
|
|
||||||
<BottomBlock.Header>
|
|
||||||
<span className="font-semibold color-gray-medium mr-4">Fetch</span>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Input
|
|
||||||
className="input-small"
|
|
||||||
placeholder="Filter"
|
|
||||||
icon="search"
|
|
||||||
iconPosition="left"
|
|
||||||
name="filter"
|
|
||||||
onChange={this.onFilterChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</BottomBlock.Header>
|
|
||||||
<BottomBlock.Content>
|
|
||||||
<NoContent title={
|
|
||||||
<div className="capitalize flex items-center mt-16">
|
|
||||||
<Icon name="info-circle" className="mr-2" size="18" />
|
|
||||||
No Data
|
|
||||||
</div>
|
|
||||||
} show={filteredList.length === 0}>
|
|
||||||
<TimeTable
|
|
||||||
rows={filteredList}
|
|
||||||
onRowClick={this.onRowClick}
|
|
||||||
hoverable
|
|
||||||
activeIndex={listNow.length - 1}
|
|
||||||
>
|
|
||||||
{[
|
|
||||||
{
|
|
||||||
label: 'Start',
|
|
||||||
width: 120,
|
|
||||||
render: renderStart,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Status',
|
|
||||||
dataKey: 'status',
|
|
||||||
width: 70,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Method',
|
|
||||||
dataKey: 'method',
|
|
||||||
width: 60,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Name',
|
|
||||||
width: 240,
|
|
||||||
render: renderName,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Time',
|
|
||||||
width: 80,
|
|
||||||
render: renderDuration,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
</TimeTable>
|
|
||||||
</NoContent>
|
|
||||||
</BottomBlock.Content>
|
|
||||||
</BottomBlock>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { default } from './Fetch';
|
|
||||||
|
|
@ -1,13 +1,32 @@
|
||||||
|
import {Duration} from "luxon";
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { NoContent, Input, SlideModal, CloseButton, Button } from 'UI';
|
import { NoContent, Input, SlideModal, CloseButton, Button } from 'UI';
|
||||||
import { getRE } from 'App/utils';
|
import { getRE } from 'App/utils';
|
||||||
import BottomBlock from '../BottomBlock';
|
import BottomBlock from '../BottomBlock';
|
||||||
import TimeTable from '../TimeTable';
|
import TimeTable from '../TimeTable';
|
||||||
import GQLDetails from './GQLDetails';
|
import GQLDetails from './GQLDetails';
|
||||||
import { renderStart } from 'Components/Session_/Network/NetworkContent';
|
|
||||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
|
export function renderStart(r) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between items-center grow-0 w-full">
|
||||||
|
<span>{Duration.fromMillis(r.time).toFormat('mm:ss.SSS')}</span>
|
||||||
|
{/* <Button
|
||||||
|
variant="text"
|
||||||
|
className="right-0 text-xs uppercase p-2 color-gray-500 hover:color-teal"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
jump(r.time);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Jump
|
||||||
|
</Button> */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function renderDefaultStatus() {
|
function renderDefaultStatus() {
|
||||||
return '2xx-3xx';
|
return '2xx-3xx';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,334 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import cn from 'classnames';
|
|
||||||
import { QuestionMarkHint, Tooltip, Tabs, Input, NoContent, Icon, Toggler } from 'UI';
|
|
||||||
import { getRE } from 'App/utils';
|
|
||||||
import { ResourceType } from 'Player';
|
|
||||||
import { formatBytes } from 'App/utils';
|
|
||||||
import { formatMs } from 'App/date';
|
|
||||||
|
|
||||||
import TimeTable from '../TimeTable';
|
|
||||||
import BottomBlock from '../BottomBlock';
|
|
||||||
import InfoLine from '../BottomBlock/InfoLine';
|
|
||||||
import stl from './network.module.css';
|
|
||||||
import { Duration } from 'luxon';
|
|
||||||
|
|
||||||
const ALL = 'ALL';
|
|
||||||
const XHR = 'xhr';
|
|
||||||
const JS = 'js';
|
|
||||||
const CSS = 'css';
|
|
||||||
const IMG = 'img';
|
|
||||||
const MEDIA = 'media';
|
|
||||||
const OTHER = 'other';
|
|
||||||
|
|
||||||
const TAB_TO_TYPE_MAP = {
|
|
||||||
[XHR]: ResourceType.XHR,
|
|
||||||
[JS]: ResourceType.SCRIPT,
|
|
||||||
[CSS]: ResourceType.CSS,
|
|
||||||
[IMG]: ResourceType.IMG,
|
|
||||||
[MEDIA]: ResourceType.MEDIA,
|
|
||||||
[OTHER]: ResourceType.OTHER,
|
|
||||||
};
|
|
||||||
const TABS = [ALL, XHR, JS, CSS, IMG, MEDIA, OTHER].map((tab) => ({
|
|
||||||
text: tab,
|
|
||||||
key: tab,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const DOM_LOADED_TIME_COLOR = 'teal';
|
|
||||||
const LOAD_TIME_COLOR = 'red';
|
|
||||||
|
|
||||||
export function renderType(r) {
|
|
||||||
return (
|
|
||||||
<Tooltip style={{ width: '100%' }} title={<div className={stl.popupNameContent}>{r.type}</div>}>
|
|
||||||
<div className={stl.popupNameTrigger}>{r.type}</div>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderName(r) {
|
|
||||||
return (
|
|
||||||
<Tooltip style={{ width: '100%' }} title={<div className={stl.popupNameContent}>{r.url}</div>}>
|
|
||||||
<div className={stl.popupNameTrigger}>{r.name}</div>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderStart(r) {
|
|
||||||
return (
|
|
||||||
<div className="flex justify-between items-center grow-0 w-full">
|
|
||||||
<span>{Duration.fromMillis(r.time).toFormat('mm:ss.SSS')}</span>
|
|
||||||
{/* <Button
|
|
||||||
variant="text"
|
|
||||||
className="right-0 text-xs uppercase p-2 color-gray-500 hover:color-teal"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
jump(r.time);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Jump
|
|
||||||
</Button> */}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderXHRText = () => (
|
|
||||||
<span className="flex items-center">
|
|
||||||
{XHR}
|
|
||||||
<QuestionMarkHint
|
|
||||||
content={
|
|
||||||
<>
|
|
||||||
Configure{' '}
|
|
||||||
<a
|
|
||||||
className="color-teal underline"
|
|
||||||
target="_blank"
|
|
||||||
href="https://docs.openreplay.com/installation/network-options"
|
|
||||||
>
|
|
||||||
Configure
|
|
||||||
</a>
|
|
||||||
network capturing
|
|
||||||
{' to see fetch/XHR requests and response payloads.'} <br />
|
|
||||||
We also provide{' '}
|
|
||||||
<a
|
|
||||||
className="color-teal underline"
|
|
||||||
target="_blank"
|
|
||||||
href="https://docs.openreplay.com/plugins/graphql"
|
|
||||||
>
|
|
||||||
support for GraphQL
|
|
||||||
</a>
|
|
||||||
{' for easy debugging of your queries.'}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
className="ml-1"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
function renderSize(r) {
|
|
||||||
if (r.responseBodySize) return formatBytes(r.responseBodySize);
|
|
||||||
let triggerText;
|
|
||||||
let content;
|
|
||||||
if (r.decodedBodySize == null) {
|
|
||||||
triggerText = 'x';
|
|
||||||
content = 'Not captured';
|
|
||||||
} else {
|
|
||||||
const headerSize = r.headerSize || 0;
|
|
||||||
const showTransferred = r.headerSize != null;
|
|
||||||
|
|
||||||
triggerText = formatBytes(r.decodedBodySize);
|
|
||||||
content = (
|
|
||||||
<ul>
|
|
||||||
{showTransferred && (
|
|
||||||
<li>{`${formatBytes(r.encodedBodySize + headerSize)} transfered over network`}</li>
|
|
||||||
)}
|
|
||||||
<li>{`Resource size: ${formatBytes(r.decodedBodySize)} `}</li>
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip style={{ width: '100%' }} content={content}>
|
|
||||||
<div>{triggerText}</div>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderDuration(r) {
|
|
||||||
if (!r.success) return 'x';
|
|
||||||
|
|
||||||
const text = `${Math.floor(r.duration)}ms`;
|
|
||||||
if (!r.isRed && !r.isYellow) return text;
|
|
||||||
|
|
||||||
let tooltipText;
|
|
||||||
let className = 'w-full h-full flex items-center ';
|
|
||||||
if (r.isYellow) {
|
|
||||||
tooltipText = 'Slower than average';
|
|
||||||
className += 'warn color-orange';
|
|
||||||
} else {
|
|
||||||
tooltipText = 'Much slower than average';
|
|
||||||
className += 'error color-red';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip style={{ width: '100%' }} content={tooltipText}>
|
|
||||||
<div className={cn(className, stl.duration)}> {text} </div>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class NetworkContent extends React.PureComponent {
|
|
||||||
state = {
|
|
||||||
filter: '',
|
|
||||||
activeTab: ALL,
|
|
||||||
};
|
|
||||||
|
|
||||||
onTabClick = (activeTab) => this.setState({ activeTab });
|
|
||||||
onFilterChange = ({ target: { value } }) => this.setState({ filter: value });
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
location,
|
|
||||||
resources,
|
|
||||||
domContentLoadedTime,
|
|
||||||
loadTime,
|
|
||||||
domBuildingTime,
|
|
||||||
fetchPresented,
|
|
||||||
onRowClick,
|
|
||||||
isResult = false,
|
|
||||||
additionalHeight = 0,
|
|
||||||
resourcesSize,
|
|
||||||
transferredSize,
|
|
||||||
time,
|
|
||||||
currentIndex,
|
|
||||||
} = this.props;
|
|
||||||
const { filter, activeTab } = this.state;
|
|
||||||
const filterRE = getRE(filter, 'i');
|
|
||||||
let filtered = resources.filter(
|
|
||||||
({ type, name }) =>
|
|
||||||
filterRE.test(name) && (activeTab === ALL || type === TAB_TO_TYPE_MAP[activeTab])
|
|
||||||
);
|
|
||||||
const lastIndex = currentIndex || filtered.filter((item) => item.time <= time).length - 1;
|
|
||||||
|
|
||||||
const referenceLines = [];
|
|
||||||
if (domContentLoadedTime != null) {
|
|
||||||
referenceLines.push({
|
|
||||||
time: domContentLoadedTime.time,
|
|
||||||
color: DOM_LOADED_TIME_COLOR,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (loadTime != null) {
|
|
||||||
referenceLines.push({
|
|
||||||
time: loadTime.time,
|
|
||||||
color: LOAD_TIME_COLOR,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let tabs = TABS;
|
|
||||||
if (!fetchPresented) {
|
|
||||||
tabs = TABS.map((tab) =>
|
|
||||||
!isResult && tab.key === XHR
|
|
||||||
? {
|
|
||||||
text: renderXHRText(),
|
|
||||||
key: XHR,
|
|
||||||
}
|
|
||||||
: tab
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<BottomBlock style={{ height: 300 + additionalHeight + 'px' }} className="border">
|
|
||||||
<BottomBlock.Header showClose={!isResult}>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="font-semibold color-gray-medium mr-4">Network</span>
|
|
||||||
<Tabs
|
|
||||||
className="uppercase"
|
|
||||||
tabs={tabs}
|
|
||||||
active={activeTab}
|
|
||||||
onClick={this.onTabClick}
|
|
||||||
border={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
className="input-small"
|
|
||||||
placeholder="Filter by name"
|
|
||||||
icon="search"
|
|
||||||
name="filter"
|
|
||||||
onChange={this.onFilterChange}
|
|
||||||
height={28}
|
|
||||||
/>
|
|
||||||
</BottomBlock.Header>
|
|
||||||
<BottomBlock.Content>
|
|
||||||
<div className="flex items-center justify-between px-4">
|
|
||||||
<div>
|
|
||||||
<Toggler checked={true} name="test" onChange={() => {}} label="4xx-5xx Only" />
|
|
||||||
</div>
|
|
||||||
<InfoLine>
|
|
||||||
<InfoLine.Point label={filtered.length} value=" requests" />
|
|
||||||
<InfoLine.Point
|
|
||||||
label={formatBytes(transferredSize)}
|
|
||||||
value="transferred"
|
|
||||||
display={transferredSize > 0}
|
|
||||||
/>
|
|
||||||
<InfoLine.Point
|
|
||||||
label={formatBytes(resourcesSize)}
|
|
||||||
value="resources"
|
|
||||||
display={resourcesSize > 0}
|
|
||||||
/>
|
|
||||||
<InfoLine.Point
|
|
||||||
label={formatMs(domBuildingTime)}
|
|
||||||
value="DOM Building Time"
|
|
||||||
display={domBuildingTime != null}
|
|
||||||
/>
|
|
||||||
<InfoLine.Point
|
|
||||||
label={domContentLoadedTime && formatMs(domContentLoadedTime.value)}
|
|
||||||
value="DOMContentLoaded"
|
|
||||||
display={domContentLoadedTime != null}
|
|
||||||
dotColor={DOM_LOADED_TIME_COLOR}
|
|
||||||
/>
|
|
||||||
<InfoLine.Point
|
|
||||||
label={loadTime && formatMs(loadTime.value)}
|
|
||||||
value="Load"
|
|
||||||
display={loadTime != null}
|
|
||||||
dotColor={LOAD_TIME_COLOR}
|
|
||||||
/>
|
|
||||||
</InfoLine>
|
|
||||||
</div>
|
|
||||||
<NoContent
|
|
||||||
title={
|
|
||||||
<div className="capitalize flex items-center mt-16">
|
|
||||||
<Icon name="info-circle" className="mr-2" size="18" />
|
|
||||||
No Data
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
size="small"
|
|
||||||
show={filtered.length === 0}
|
|
||||||
>
|
|
||||||
<TimeTable
|
|
||||||
rows={filtered}
|
|
||||||
referenceLines={referenceLines}
|
|
||||||
renderPopup
|
|
||||||
// navigation
|
|
||||||
onRowClick={onRowClick}
|
|
||||||
additionalHeight={additionalHeight}
|
|
||||||
activeIndex={lastIndex}
|
|
||||||
>
|
|
||||||
{[
|
|
||||||
// {
|
|
||||||
// label: 'Start',
|
|
||||||
// width: 120,
|
|
||||||
// render: renderStart,
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
label: 'Status',
|
|
||||||
dataKey: 'status',
|
|
||||||
width: 70,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Type',
|
|
||||||
dataKey: 'type',
|
|
||||||
width: 90,
|
|
||||||
render: renderType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Name',
|
|
||||||
width: 240,
|
|
||||||
render: renderName,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Size',
|
|
||||||
width: 60,
|
|
||||||
render: renderSize,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Time',
|
|
||||||
width: 80,
|
|
||||||
render: renderDuration,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
</TimeTable>
|
|
||||||
</NoContent>
|
|
||||||
</BottomBlock.Content>
|
|
||||||
</BottomBlock>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
.location {
|
|
||||||
min-height: 32px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 5px 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 12px;
|
|
||||||
|
|
||||||
& > div:last-child {
|
|
||||||
font-weight: 300;
|
|
||||||
max-width: 80%;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
padding: 5px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.popupNameTrigger {
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
max-width: 80%;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
.popupNameContent {
|
|
||||||
max-width: 600px;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
@ -92,7 +92,7 @@ function renderSize(r: any) {
|
||||||
content = (
|
content = (
|
||||||
<ul>
|
<ul>
|
||||||
{showTransferred && (
|
{showTransferred && (
|
||||||
<li>{`${formatBytes(r.encodedBodySize + headerSize)} transfered over network`}</li>
|
<li>{`${formatBytes(r.encodedBodySize + headerSize)} transferred over network`}</li>
|
||||||
)}
|
)}
|
||||||
<li>{`Resource size: ${formatBytes(r.decodedBodySize)} `}</li>
|
<li>{`Resource size: ${formatBytes(r.decodedBodySize)} `}</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -211,7 +211,7 @@ export default class RawMessageReader extends PrimitiveReader {
|
||||||
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
|
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
|
||||||
const duration = this.readUint(); if (duration === null) { return resetPointer() }
|
const duration = this.readUint(); if (duration === null) { return resetPointer() }
|
||||||
return {
|
return {
|
||||||
tp: MType.NetworkRequest,
|
tp: MType.NetworkRequestDeprecated,
|
||||||
type,
|
type,
|
||||||
method,
|
method,
|
||||||
url,
|
url,
|
||||||
|
|
@ -627,6 +627,30 @@ export default class RawMessageReader extends PrimitiveReader {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 83: {
|
||||||
|
const type = this.readString(); if (type === null) { return resetPointer() }
|
||||||
|
const method = this.readString(); if (method === null) { return resetPointer() }
|
||||||
|
const url = this.readString(); if (url === null) { return resetPointer() }
|
||||||
|
const request = this.readString(); if (request === null) { return resetPointer() }
|
||||||
|
const response = this.readString(); if (response === null) { return resetPointer() }
|
||||||
|
const status = this.readUint(); if (status === null) { return resetPointer() }
|
||||||
|
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
|
||||||
|
const duration = this.readUint(); if (duration === null) { return resetPointer() }
|
||||||
|
const transferredBodySize = this.readUint(); if (transferredBodySize === null) { return resetPointer() }
|
||||||
|
return {
|
||||||
|
tp: MType.NetworkRequest,
|
||||||
|
type,
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
request,
|
||||||
|
response,
|
||||||
|
status,
|
||||||
|
timestamp,
|
||||||
|
duration,
|
||||||
|
transferredBodySize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
case 113: {
|
case 113: {
|
||||||
const selectionStart = this.readUint(); if (selectionStart === null) { return resetPointer() }
|
const selectionStart = this.readUint(); if (selectionStart === null) { return resetPointer() }
|
||||||
const selectionEnd = this.readUint(); if (selectionEnd === null) { return resetPointer() }
|
const selectionEnd = this.readUint(); if (selectionEnd === null) { return resetPointer() }
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import type {
|
||||||
RawSetInputValue,
|
RawSetInputValue,
|
||||||
RawSetInputChecked,
|
RawSetInputChecked,
|
||||||
RawMouseMove,
|
RawMouseMove,
|
||||||
RawNetworkRequest,
|
RawNetworkRequestDeprecated,
|
||||||
RawConsoleLog,
|
RawConsoleLog,
|
||||||
RawCssInsertRule,
|
RawCssInsertRule,
|
||||||
RawCssDeleteRule,
|
RawCssDeleteRule,
|
||||||
|
|
@ -55,6 +55,7 @@ import type {
|
||||||
RawAdoptedSsAddOwner,
|
RawAdoptedSsAddOwner,
|
||||||
RawAdoptedSsRemoveOwner,
|
RawAdoptedSsRemoveOwner,
|
||||||
RawZustand,
|
RawZustand,
|
||||||
|
RawNetworkRequest,
|
||||||
RawSelectionChange,
|
RawSelectionChange,
|
||||||
RawMouseThrashing,
|
RawMouseThrashing,
|
||||||
RawResourceTiming,
|
RawResourceTiming,
|
||||||
|
|
@ -107,7 +108,7 @@ export type SetInputChecked = RawSetInputChecked & Timed
|
||||||
|
|
||||||
export type MouseMove = RawMouseMove & Timed
|
export type MouseMove = RawMouseMove & Timed
|
||||||
|
|
||||||
export type NetworkRequest = RawNetworkRequest & Timed
|
export type NetworkRequestDeprecated = RawNetworkRequestDeprecated & Timed
|
||||||
|
|
||||||
export type ConsoleLog = RawConsoleLog & Timed
|
export type ConsoleLog = RawConsoleLog & Timed
|
||||||
|
|
||||||
|
|
@ -175,6 +176,8 @@ export type AdoptedSsRemoveOwner = RawAdoptedSsRemoveOwner & Timed
|
||||||
|
|
||||||
export type Zustand = RawZustand & Timed
|
export type Zustand = RawZustand & Timed
|
||||||
|
|
||||||
|
export type NetworkRequest = RawNetworkRequest & Timed
|
||||||
|
|
||||||
export type SelectionChange = RawSelectionChange & Timed
|
export type SelectionChange = RawSelectionChange & Timed
|
||||||
|
|
||||||
export type MouseThrashing = RawMouseThrashing & Timed
|
export type MouseThrashing = RawMouseThrashing & Timed
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ export const enum MType {
|
||||||
SetInputValue = 18,
|
SetInputValue = 18,
|
||||||
SetInputChecked = 19,
|
SetInputChecked = 19,
|
||||||
MouseMove = 20,
|
MouseMove = 20,
|
||||||
NetworkRequest = 21,
|
NetworkRequestDeprecated = 21,
|
||||||
ConsoleLog = 22,
|
ConsoleLog = 22,
|
||||||
CssInsertRule = 37,
|
CssInsertRule = 37,
|
||||||
CssDeleteRule = 38,
|
CssDeleteRule = 38,
|
||||||
|
|
@ -53,6 +53,7 @@ export const enum MType {
|
||||||
AdoptedSsAddOwner = 76,
|
AdoptedSsAddOwner = 76,
|
||||||
AdoptedSsRemoveOwner = 77,
|
AdoptedSsRemoveOwner = 77,
|
||||||
Zustand = 79,
|
Zustand = 79,
|
||||||
|
NetworkRequest = 83,
|
||||||
SelectionChange = 113,
|
SelectionChange = 113,
|
||||||
MouseThrashing = 114,
|
MouseThrashing = 114,
|
||||||
ResourceTiming = 116,
|
ResourceTiming = 116,
|
||||||
|
|
@ -177,8 +178,8 @@ export interface RawMouseMove {
|
||||||
y: number,
|
y: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RawNetworkRequest {
|
export interface RawNetworkRequestDeprecated {
|
||||||
tp: MType.NetworkRequest,
|
tp: MType.NetworkRequestDeprecated,
|
||||||
type: string,
|
type: string,
|
||||||
method: string,
|
method: string,
|
||||||
url: string,
|
url: string,
|
||||||
|
|
@ -424,6 +425,19 @@ export interface RawZustand {
|
||||||
state: string,
|
state: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RawNetworkRequest {
|
||||||
|
tp: MType.NetworkRequest,
|
||||||
|
type: string,
|
||||||
|
method: string,
|
||||||
|
url: string,
|
||||||
|
request: string,
|
||||||
|
response: string,
|
||||||
|
status: number,
|
||||||
|
timestamp: number,
|
||||||
|
duration: number,
|
||||||
|
transferredBodySize: number,
|
||||||
|
}
|
||||||
|
|
||||||
export interface RawSelectionChange {
|
export interface RawSelectionChange {
|
||||||
tp: MType.SelectionChange,
|
tp: MType.SelectionChange,
|
||||||
selectionStart: number,
|
selectionStart: number,
|
||||||
|
|
@ -536,4 +550,4 @@ export interface RawIosSwipeEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export type RawMessage = RawTimestamp | RawSetPageLocation | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawNetworkRequest | RawConsoleLog | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawRedux | RawVuex | RawMobX | RawNgRx | RawGraphQl | RawPerformanceTrack | RawStringDict | RawSetNodeAttributeDict | RawResourceTimingDeprecated | RawConnectionInformation | RawSetPageVisibility | RawLoadFontFace | RawSetNodeFocus | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawSelectionChange | RawMouseThrashing | RawResourceTiming | RawTabChange | RawTabData | RawIosCustomEvent | RawIosScreenChanges | RawIosClickEvent | RawIosInputEvent | RawIosPerformanceEvent | RawIosLog | RawIosNetworkCall | RawIosSwipeEvent;
|
export type RawMessage = RawTimestamp | RawSetPageLocation | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawNetworkRequestDeprecated | RawConsoleLog | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawRedux | RawVuex | RawMobX | RawNgRx | RawGraphQl | RawPerformanceTrack | RawStringDict | RawSetNodeAttributeDict | RawResourceTimingDeprecated | RawConnectionInformation | RawSetPageVisibility | RawLoadFontFace | RawSetNodeFocus | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawNetworkRequest | RawSelectionChange | RawMouseThrashing | RawResourceTiming | RawTabChange | RawTabData | RawIosCustomEvent | RawIosScreenChanges | RawIosClickEvent | RawIosInputEvent | RawIosPerformanceEvent | RawIosLog | RawIosNetworkCall | RawIosSwipeEvent;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export const TP_MAP = {
|
||||||
18: MType.SetInputValue,
|
18: MType.SetInputValue,
|
||||||
19: MType.SetInputChecked,
|
19: MType.SetInputChecked,
|
||||||
20: MType.MouseMove,
|
20: MType.MouseMove,
|
||||||
21: MType.NetworkRequest,
|
21: MType.NetworkRequestDeprecated,
|
||||||
22: MType.ConsoleLog,
|
22: MType.ConsoleLog,
|
||||||
37: MType.CssInsertRule,
|
37: MType.CssInsertRule,
|
||||||
38: MType.CssDeleteRule,
|
38: MType.CssDeleteRule,
|
||||||
|
|
@ -54,6 +54,7 @@ export const TP_MAP = {
|
||||||
76: MType.AdoptedSsAddOwner,
|
76: MType.AdoptedSsAddOwner,
|
||||||
77: MType.AdoptedSsRemoveOwner,
|
77: MType.AdoptedSsRemoveOwner,
|
||||||
79: MType.Zustand,
|
79: MType.Zustand,
|
||||||
|
83: MType.NetworkRequest,
|
||||||
113: MType.SelectionChange,
|
113: MType.SelectionChange,
|
||||||
114: MType.MouseThrashing,
|
114: MType.MouseThrashing,
|
||||||
116: MType.ResourceTiming,
|
116: MType.ResourceTiming,
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ type TrMouseMove = [
|
||||||
y: number,
|
y: number,
|
||||||
]
|
]
|
||||||
|
|
||||||
type TrNetworkRequest = [
|
type TrNetworkRequestDeprecated = [
|
||||||
type: 21,
|
type: 21,
|
||||||
type: string,
|
type: string,
|
||||||
method: string,
|
method: string,
|
||||||
|
|
@ -429,6 +429,19 @@ type TrPartitionedMessage = [
|
||||||
partTotal: number,
|
partTotal: number,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
type TrNetworkRequest = [
|
||||||
|
type: 83,
|
||||||
|
type: string,
|
||||||
|
method: string,
|
||||||
|
url: string,
|
||||||
|
request: string,
|
||||||
|
response: string,
|
||||||
|
status: number,
|
||||||
|
timestamp: number,
|
||||||
|
duration: number,
|
||||||
|
transferredBodySize: number,
|
||||||
|
]
|
||||||
|
|
||||||
type TrInputChange = [
|
type TrInputChange = [
|
||||||
type: 112,
|
type: 112,
|
||||||
id: number,
|
id: number,
|
||||||
|
|
@ -481,7 +494,7 @@ type TrTabData = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
export type TrackerMessage = TrTimestamp | TrSetPageLocation | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrNetworkRequest | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrRedux | TrVuex | TrMobX | TrNgRx | TrGraphQL | TrPerformanceTrack | TrStringDict | TrSetNodeAttributeDict | TrResourceTimingDeprecated | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrJSException | TrZustand | TrBatchMetadata | TrPartitionedMessage | TrInputChange | TrSelectionChange | TrMouseThrashing | TrUnbindNodes | TrResourceTiming | TrTabChange | TrTabData
|
export type TrackerMessage = TrTimestamp | TrSetPageLocation | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrNetworkRequestDeprecated | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrRedux | TrVuex | TrMobX | TrNgRx | TrGraphQL | TrPerformanceTrack | TrStringDict | TrSetNodeAttributeDict | TrResourceTimingDeprecated | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrJSException | TrZustand | TrBatchMetadata | TrPartitionedMessage | TrNetworkRequest | TrInputChange | TrSelectionChange | TrMouseThrashing | TrUnbindNodes | TrResourceTiming | TrTabChange | TrTabData
|
||||||
|
|
||||||
export default function translate(tMsg: TrackerMessage): RawMessage | null {
|
export default function translate(tMsg: TrackerMessage): RawMessage | null {
|
||||||
switch(tMsg[0]) {
|
switch(tMsg[0]) {
|
||||||
|
|
@ -622,7 +635,7 @@ export default function translate(tMsg: TrackerMessage): RawMessage | null {
|
||||||
|
|
||||||
case 21: {
|
case 21: {
|
||||||
return {
|
return {
|
||||||
tp: MType.NetworkRequest,
|
tp: MType.NetworkRequestDeprecated,
|
||||||
type: tMsg[1],
|
type: tMsg[1],
|
||||||
method: tMsg[2],
|
method: tMsg[2],
|
||||||
url: tMsg[3],
|
url: tMsg[3],
|
||||||
|
|
@ -918,6 +931,21 @@ export default function translate(tMsg: TrackerMessage): RawMessage | null {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 83: {
|
||||||
|
return {
|
||||||
|
tp: MType.NetworkRequest,
|
||||||
|
type: tMsg[1],
|
||||||
|
method: tMsg[2],
|
||||||
|
url: tMsg[3],
|
||||||
|
request: tMsg[4],
|
||||||
|
response: tMsg[5],
|
||||||
|
status: tMsg[6],
|
||||||
|
timestamp: tMsg[7],
|
||||||
|
duration: tMsg[8],
|
||||||
|
transferredBodySize: tMsg[9],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case 113: {
|
case 113: {
|
||||||
return {
|
return {
|
||||||
tp: MType.SelectionChange,
|
tp: MType.SelectionChange,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import type { ResourceTiming, NetworkRequest, Fetch } from '../messages'
|
||||||
export const enum ResourceType {
|
export const enum ResourceType {
|
||||||
XHR = 'xhr',
|
XHR = 'xhr',
|
||||||
FETCH = 'fetch',
|
FETCH = 'fetch',
|
||||||
|
BEACON = 'beacon',
|
||||||
SCRIPT = 'script',
|
SCRIPT = 'script',
|
||||||
CSS = 'css',
|
CSS = 'css',
|
||||||
IMG = 'img',
|
IMG = 'img',
|
||||||
|
|
@ -10,17 +11,19 @@ export const enum ResourceType {
|
||||||
OTHER = 'other',
|
OTHER = 'other',
|
||||||
}
|
}
|
||||||
|
|
||||||
function getURLExtention(url: string): string {
|
export function getURLExtention(url: string): string {
|
||||||
const pts = url.split("?")[0].split(".")
|
const pts = url.split("?")[0].split(".")
|
||||||
return pts[pts.length-1] || ""
|
return pts[pts.length-1] || ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// maybe move this thing to the tracker
|
// maybe move this thing to the tracker
|
||||||
function getResourceType(initiator: string, url: string): ResourceType {
|
export function getResourceType(initiator: string, url: string): ResourceType {
|
||||||
switch (initiator) {
|
switch (initiator) {
|
||||||
case "xmlhttprequest":
|
case "xmlhttprequest":
|
||||||
case "fetch":
|
case "fetch":
|
||||||
return ResourceType.FETCH
|
return ResourceType.FETCH
|
||||||
|
case "beacon":
|
||||||
|
return ResourceType.BEACON
|
||||||
case "img":
|
case "img":
|
||||||
return ResourceType.IMG
|
return ResourceType.IMG
|
||||||
default:
|
default:
|
||||||
|
|
@ -48,7 +51,7 @@ function getResourceType(initiator: string, url: string): ResourceType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getResourceName(url: string) {
|
export function getResourceName(url: string) {
|
||||||
return url
|
return url
|
||||||
.split('/')
|
.split('/')
|
||||||
.filter((s) => s !== '')
|
.filter((s) => s !== '')
|
||||||
|
|
@ -104,10 +107,11 @@ export function getResourceFromNetworkRequest(msg: NetworkRequest | Fetch, sessS
|
||||||
return Resource({
|
return Resource({
|
||||||
...msg,
|
...msg,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
type: msg?.type === "xhr" ? ResourceType.XHR : ResourceType.FETCH,
|
type: msg?.type ? msg?.type : ResourceType.XHR,
|
||||||
success: msg.status < 400,
|
success: msg.status < 400,
|
||||||
status: String(msg.status),
|
status: String(msg.status),
|
||||||
time: Math.max(0, msg.timestamp - sessStart),
|
time: Math.max(0, msg.timestamp - sessStart),
|
||||||
|
decodedBodySize: 'transferredBodySize' in msg ? msg.transferredBodySize : undefined
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,26 @@
|
||||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
console.log(__dirname)
|
||||||
|
const dir = __dirname
|
||||||
module.exports = {
|
module.exports = {
|
||||||
preset: 'ts-jest',
|
preset: 'ts-jest',
|
||||||
rootDir: './',
|
|
||||||
testEnvironment: 'jsdom',
|
testEnvironment: 'jsdom',
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'^Types/session/(.+)$': '<rootDir>/app/types/session/$1',
|
'^Types/session/(.+)$': '<rootDir>/app/types/session/$1',
|
||||||
'^App/(.+)$': '<rootDir>/app/$1',
|
'^App/(.+)$': '<rootDir>/app/$1',
|
||||||
},
|
},
|
||||||
|
collectCoverage: true,
|
||||||
|
verbose: true,
|
||||||
|
collectCoverageFrom: [
|
||||||
|
// '<rootDir>/app/**/*.{ts,tsx,js,jsx}',
|
||||||
|
'<rootDir>/app/player/**/*.{ts,tsx,js,jsx}',
|
||||||
|
'<rootDir>/app/mstore/**/*.{ts,tsx,js,jsx}',
|
||||||
|
'<rootDir>/app/utils/**/*.{ts,tsx,js,jsx}',
|
||||||
|
|
||||||
|
'!<rootDir>/app/**/*.d.ts',
|
||||||
|
'!<rootDir>/node_modules'
|
||||||
|
],
|
||||||
transform: {
|
transform: {
|
||||||
'^.+\\.(ts|tsx)?$': ['ts-jest', { isolatedModules: true, diagnostics: { warnOnly: true } }],
|
'^.+\\.(ts|tsx)?$': ['ts-jest', { isolatedModules: true, diagnostics: { warnOnly: true } }],
|
||||||
'^.+\\.(js|jsx)$': 'babel-jest',
|
'^.+\\.(js|jsx)$': 'babel-jest',
|
||||||
},
|
},
|
||||||
moduleDirectories: ['node_modules', 'app'],
|
moduleDirectories: ['node_modules'],
|
||||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
|
||||||
// module.exports = {
|
|
||||||
// globals: {
|
|
||||||
// "ts-jest": {
|
|
||||||
// tsConfig: "tsconfig.json",
|
|
||||||
// diagnostics: true
|
|
||||||
// },
|
|
||||||
// NODE_ENV: "test"
|
|
||||||
// },
|
|
||||||
// moduleNameMapper: {
|
|
||||||
// "^Types/(.+)$": "<rootDir>/app/types/$1"
|
|
||||||
// },
|
|
||||||
// moduleDirectories: ["node_modules", 'app'],
|
|
||||||
// moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json"],
|
|
||||||
|
|
||||||
// verbose: true
|
|
||||||
// };
|
|
||||||
|
|
@ -16,7 +16,8 @@
|
||||||
"flow": "flow",
|
"flow": "flow",
|
||||||
"gen:static": "yarn gen:icons && yarn gen:colors",
|
"gen:static": "yarn gen:icons && yarn gen:colors",
|
||||||
"build-storybook": "build-storybook",
|
"build-storybook": "build-storybook",
|
||||||
"test": "jest",
|
"test:ci": "jest --maxWorkers=1 --no-cache --coverage",
|
||||||
|
"test": "jest --watch",
|
||||||
"cy:open": "cypress open",
|
"cy:open": "cypress open",
|
||||||
"cy:test": "cypress run --browser chrome",
|
"cy:test": "cypress run --browser chrome",
|
||||||
"cy:test-firefox": "cypress run --browser firefox",
|
"cy:test-firefox": "cypress run --browser firefox",
|
||||||
|
|
@ -94,7 +95,7 @@
|
||||||
"@babel/preset-react": "^7.17.12",
|
"@babel/preset-react": "^7.17.12",
|
||||||
"@babel/preset-typescript": "^7.17.12",
|
"@babel/preset-typescript": "^7.17.12",
|
||||||
"@babel/runtime": "^7.17.9",
|
"@babel/runtime": "^7.17.9",
|
||||||
"@jest/globals": "^29.3.1",
|
"@jest/globals": "^29.5.0",
|
||||||
"@openreplay/sourcemap-uploader": "^3.0.0",
|
"@openreplay/sourcemap-uploader": "^3.0.0",
|
||||||
"@storybook/addon-actions": "^6.5.12",
|
"@storybook/addon-actions": "^6.5.12",
|
||||||
"@storybook/addon-docs": "^6.5.12",
|
"@storybook/addon-docs": "^6.5.12",
|
||||||
|
|
@ -132,7 +133,7 @@
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"flow-bin": "^0.115.0",
|
"flow-bin": "^0.115.0",
|
||||||
"html-webpack-plugin": "^5.5.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"jest": "^29.3.1",
|
"jest": "^29.5.0",
|
||||||
"mini-css-extract-plugin": "^2.6.0",
|
"mini-css-extract-plugin": "^2.6.0",
|
||||||
"minio": "^7.0.18",
|
"minio": "^7.0.18",
|
||||||
"moment-locales-webpack-plugin": "^1.2.0",
|
"moment-locales-webpack-plugin": "^1.2.0",
|
||||||
|
|
|
||||||
118
frontend/tests/types.resource.test.ts
Normal file
118
frontend/tests/types.resource.test.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
import {
|
||||||
|
ResourceType,
|
||||||
|
getURLExtention,
|
||||||
|
getResourceType,
|
||||||
|
getResourceName,
|
||||||
|
Resource,
|
||||||
|
getResourceFromResourceTiming,
|
||||||
|
getResourceFromNetworkRequest,
|
||||||
|
} from '../app/player/web/types/resource';
|
||||||
|
import type { ResourceTiming, NetworkRequest } from '../app/player/web/messages';
|
||||||
|
import { test, describe, expect } from "@jest/globals";
|
||||||
|
|
||||||
|
describe('getURLExtention', () => {
|
||||||
|
test('should return the correct extension', () => {
|
||||||
|
expect(getURLExtention('https://test.com/image.png')).toBe('png');
|
||||||
|
expect(getURLExtention('https://test.com/script.js')).toBe('js');
|
||||||
|
expect(getURLExtention('https://test.com/style.css')).toBe('css');
|
||||||
|
expect(getURLExtention('https://test.com')).toBe('com');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getResourceType', () => {
|
||||||
|
test('should return the correct resource type based on initiator and URL', () => {
|
||||||
|
expect(getResourceType('fetch', 'https://test.com')).toBe(ResourceType.FETCH);
|
||||||
|
expect(getResourceType('beacon', 'https://test.com')).toBe(ResourceType.BEACON);
|
||||||
|
expect(getResourceType('img', 'https://test.com')).toBe(ResourceType.IMG);
|
||||||
|
expect(getResourceType('unknown', 'https://test.com/script.js')).toBe(ResourceType.SCRIPT);
|
||||||
|
expect(getResourceType('unknown', 'https://test.com/style.css')).toBe(ResourceType.CSS);
|
||||||
|
expect(getResourceType('unknown', 'https://test.com/image.png')).toBe(ResourceType.IMG);
|
||||||
|
expect(getResourceType('unknown', 'https://test.com/video.mp4')).toBe(ResourceType.MEDIA);
|
||||||
|
expect(getResourceType('unknown', 'https://test.com')).toBe(ResourceType.OTHER);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getResourceName', () => {
|
||||||
|
test('should return the last non-empty section of a URL', () => {
|
||||||
|
expect(getResourceName('https://test.com/path/to/resource')).toBe('resource');
|
||||||
|
expect(getResourceName('https://test.com/another/path/')).toBe('path');
|
||||||
|
expect(getResourceName('https://test.com/singlepath')).toBe('singlepath');
|
||||||
|
expect(getResourceName('https://test.com/')).toBe('test.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Resource', () => {
|
||||||
|
test('should return the correct resource object', () => {
|
||||||
|
const testResource = {
|
||||||
|
time: 123,
|
||||||
|
type: ResourceType.SCRIPT,
|
||||||
|
url: 'https://test.com/script.js',
|
||||||
|
status: '2xx-3xx',
|
||||||
|
method: 'GET',
|
||||||
|
duration: 1,
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
const expectedResult = {
|
||||||
|
...testResource,
|
||||||
|
name: 'script.js',
|
||||||
|
isRed: false,
|
||||||
|
isYellow: false,
|
||||||
|
};
|
||||||
|
expect(Resource(testResource)).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getResourceFromResourceTiming', () => {
|
||||||
|
test('should return the correct resource from a ResourceTiming object', () => {
|
||||||
|
const testResourceTiming: ResourceTiming = {
|
||||||
|
tp: 116,
|
||||||
|
timestamp: 123,
|
||||||
|
duration: 1,
|
||||||
|
ttfb: 100,
|
||||||
|
headerSize: 200,
|
||||||
|
encodedBodySize: 300,
|
||||||
|
decodedBodySize: 400,
|
||||||
|
url: 'https://test.com/script.js',
|
||||||
|
initiator: 'fetch',
|
||||||
|
transferredSize: 500,
|
||||||
|
cached: false,
|
||||||
|
time: 123
|
||||||
|
};
|
||||||
|
const expectedResult = Resource({
|
||||||
|
...testResourceTiming,
|
||||||
|
type: ResourceType.FETCH,
|
||||||
|
method: '..',
|
||||||
|
success: true,
|
||||||
|
status: '2xx-3xx',
|
||||||
|
time: 123,
|
||||||
|
});
|
||||||
|
expect(getResourceFromResourceTiming(testResourceTiming, 0)).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getResourceFromNetworkRequest', () => {
|
||||||
|
test('should return the correct resource from a NetworkRequest or Fetch object', () => {
|
||||||
|
const testNetworkRequest: NetworkRequest = {
|
||||||
|
tp: 83,
|
||||||
|
type: 'fetch',
|
||||||
|
method: 'POST',
|
||||||
|
url: 'https://test.com/data',
|
||||||
|
request: 'test',
|
||||||
|
response: 'test',
|
||||||
|
status: 200,
|
||||||
|
timestamp: 123,
|
||||||
|
duration: 1,
|
||||||
|
transferredBodySize: 100,
|
||||||
|
time: 123
|
||||||
|
} as const;
|
||||||
|
// @ts-ignore
|
||||||
|
const expectedResult = Resource({
|
||||||
|
...testNetworkRequest,
|
||||||
|
success: true,
|
||||||
|
status: '200',
|
||||||
|
time: 123,
|
||||||
|
decodedBodySize: 100,
|
||||||
|
});
|
||||||
|
expect(getResourceFromNetworkRequest(testNetworkRequest, 0)).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -104,7 +104,7 @@ message 20, 'MouseMove' do
|
||||||
uint 'X'
|
uint 'X'
|
||||||
uint 'Y'
|
uint 'Y'
|
||||||
end
|
end
|
||||||
message 21, 'NetworkRequest', :replayer => :devtools do
|
message 21, 'NetworkRequestDeprecated', :replayer => :devtools do
|
||||||
string 'Type' # fetch/xhr/anythingElse(axios,gql,fonts,image?)
|
string 'Type' # fetch/xhr/anythingElse(axios,gql,fonts,image?)
|
||||||
string 'Method'
|
string 'Method'
|
||||||
string 'URL'
|
string 'URL'
|
||||||
|
|
@ -446,6 +446,18 @@ message 82, 'PartitionedMessage', :replayer => false do
|
||||||
uint 'PartTotal'
|
uint 'PartTotal'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
message 83, 'NetworkRequest', :replayer => :devtools do
|
||||||
|
string 'Type' # fetch/xhr/anythingElse(axios,gql,fonts,image?)
|
||||||
|
string 'Method'
|
||||||
|
string 'URL'
|
||||||
|
string 'Request'
|
||||||
|
string 'Response'
|
||||||
|
uint 'Status'
|
||||||
|
uint 'Timestamp'
|
||||||
|
uint 'Duration'
|
||||||
|
uint 'TransferredBodySize'
|
||||||
|
end
|
||||||
|
|
||||||
# 90-111 reserved iOS
|
# 90-111 reserved iOS
|
||||||
|
|
||||||
message 112, 'InputChange', :replayer => false do
|
message 112, 'InputChange', :replayer => false do
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
|
# 10.0.0
|
||||||
|
|
||||||
|
- networkRequest message changed to include `TransferredBodySize`
|
||||||
|
- tracker now attempts to create proxy for beacon api as well (if its in scope)
|
||||||
|
|
||||||
# 9.0.9
|
# 9.0.9
|
||||||
|
|
||||||
- Fix for `{disableStringDict: true}` behavior
|
- Fix for `{disableStringDict: true}` behavior
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@openreplay/tracker",
|
"name": "@openreplay/tracker",
|
||||||
"description": "The OpenReplay tracker main package",
|
"description": "The OpenReplay tracker main package",
|
||||||
"version": "9.0.9",
|
"version": "10.0.0",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"logging",
|
"logging",
|
||||||
"replay"
|
"replay"
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ export declare const enum Type {
|
||||||
SetInputValue = 18,
|
SetInputValue = 18,
|
||||||
SetInputChecked = 19,
|
SetInputChecked = 19,
|
||||||
MouseMove = 20,
|
MouseMove = 20,
|
||||||
NetworkRequest = 21,
|
NetworkRequestDeprecated = 21,
|
||||||
ConsoleLog = 22,
|
ConsoleLog = 22,
|
||||||
PageLoadTiming = 23,
|
PageLoadTiming = 23,
|
||||||
PageRenderTiming = 24,
|
PageRenderTiming = 24,
|
||||||
|
|
@ -63,6 +63,7 @@ export declare const enum Type {
|
||||||
Zustand = 79,
|
Zustand = 79,
|
||||||
BatchMetadata = 81,
|
BatchMetadata = 81,
|
||||||
PartitionedMessage = 82,
|
PartitionedMessage = 82,
|
||||||
|
NetworkRequest = 83,
|
||||||
InputChange = 112,
|
InputChange = 112,
|
||||||
SelectionChange = 113,
|
SelectionChange = 113,
|
||||||
MouseThrashing = 114,
|
MouseThrashing = 114,
|
||||||
|
|
@ -181,8 +182,8 @@ export type MouseMove = [
|
||||||
/*y:*/ number,
|
/*y:*/ number,
|
||||||
]
|
]
|
||||||
|
|
||||||
export type NetworkRequest = [
|
export type NetworkRequestDeprecated = [
|
||||||
/*type:*/ Type.NetworkRequest,
|
/*type:*/ Type.NetworkRequestDeprecated,
|
||||||
/*type:*/ string,
|
/*type:*/ string,
|
||||||
/*method:*/ string,
|
/*method:*/ string,
|
||||||
/*url:*/ string,
|
/*url:*/ string,
|
||||||
|
|
@ -497,6 +498,19 @@ export type PartitionedMessage = [
|
||||||
/*partTotal:*/ number,
|
/*partTotal:*/ number,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export type NetworkRequest = [
|
||||||
|
/*type:*/ Type.NetworkRequest,
|
||||||
|
/*type:*/ string,
|
||||||
|
/*method:*/ string,
|
||||||
|
/*url:*/ string,
|
||||||
|
/*request:*/ string,
|
||||||
|
/*response:*/ string,
|
||||||
|
/*status:*/ number,
|
||||||
|
/*timestamp:*/ number,
|
||||||
|
/*duration:*/ number,
|
||||||
|
/*transferredBodySize:*/ number,
|
||||||
|
]
|
||||||
|
|
||||||
export type InputChange = [
|
export type InputChange = [
|
||||||
/*type:*/ Type.InputChange,
|
/*type:*/ Type.InputChange,
|
||||||
/*id:*/ number,
|
/*id:*/ number,
|
||||||
|
|
@ -549,5 +563,5 @@ export type TabData = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
type Message = Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | NetworkRequest | ConsoleLog | PageLoadTiming | PageRenderTiming | CustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | Redux | Vuex | MobX | NgRx | GraphQL | PerformanceTrack | StringDict | SetNodeAttributeDict | ResourceTimingDeprecated | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | JSException | Zustand | BatchMetadata | PartitionedMessage | InputChange | SelectionChange | MouseThrashing | UnbindNodes | ResourceTiming | TabChange | TabData
|
type Message = Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | NetworkRequestDeprecated | ConsoleLog | PageLoadTiming | PageRenderTiming | CustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | Redux | Vuex | MobX | NgRx | GraphQL | PerformanceTrack | StringDict | SetNodeAttributeDict | ResourceTimingDeprecated | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | JSException | Zustand | BatchMetadata | PartitionedMessage | NetworkRequest | InputChange | SelectionChange | MouseThrashing | UnbindNodes | ResourceTiming | TabChange | TabData
|
||||||
export default Message
|
export default Message
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,7 @@ export function MouseMove(
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NetworkRequest(
|
export function NetworkRequestDeprecated(
|
||||||
type: string,
|
type: string,
|
||||||
method: string,
|
method: string,
|
||||||
url: string,
|
url: string,
|
||||||
|
|
@ -213,9 +213,9 @@ export function NetworkRequest(
|
||||||
status: number,
|
status: number,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
duration: number,
|
duration: number,
|
||||||
): Messages.NetworkRequest {
|
): Messages.NetworkRequestDeprecated {
|
||||||
return [
|
return [
|
||||||
Messages.Type.NetworkRequest,
|
Messages.Type.NetworkRequestDeprecated,
|
||||||
type,
|
type,
|
||||||
method,
|
method,
|
||||||
url,
|
url,
|
||||||
|
|
@ -792,6 +792,31 @@ export function PartitionedMessage(
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function NetworkRequest(
|
||||||
|
type: string,
|
||||||
|
method: string,
|
||||||
|
url: string,
|
||||||
|
request: string,
|
||||||
|
response: string,
|
||||||
|
status: number,
|
||||||
|
timestamp: number,
|
||||||
|
duration: number,
|
||||||
|
transferredBodySize: number,
|
||||||
|
): Messages.NetworkRequest {
|
||||||
|
return [
|
||||||
|
Messages.Type.NetworkRequest,
|
||||||
|
type,
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
request,
|
||||||
|
response,
|
||||||
|
status,
|
||||||
|
timestamp,
|
||||||
|
duration,
|
||||||
|
transferredBodySize,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
export function InputChange(
|
export function InputChange(
|
||||||
id: number,
|
id: number,
|
||||||
value: string,
|
value: string,
|
||||||
|
|
|
||||||
102
tracker/tracker/src/main/modules/Network/beaconProxy.ts
Normal file
102
tracker/tracker/src/main/modules/Network/beaconProxy.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
import { NetworkRequest } from '../../../common/messages.gen.js'
|
||||||
|
import NetworkMessage from './networkMessage.js'
|
||||||
|
import { RequestResponseData } from './types.js'
|
||||||
|
import { genStringBody, getURL } from './utils.js'
|
||||||
|
|
||||||
|
// https://fetch.spec.whatwg.org/#concept-bodyinit-extract
|
||||||
|
const getContentType = (data?: BodyInit) => {
|
||||||
|
if (data instanceof Blob) {
|
||||||
|
return data.type
|
||||||
|
}
|
||||||
|
if (data instanceof FormData) {
|
||||||
|
return 'multipart/form-data'
|
||||||
|
}
|
||||||
|
if (data instanceof URLSearchParams) {
|
||||||
|
return 'application/x-www-form-urlencoded;charset=UTF-8'
|
||||||
|
}
|
||||||
|
return 'text/plain;charset=UTF-8'
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BeaconProxyHandler<T extends typeof navigator.sendBeacon> implements ProxyHandler<T> {
|
||||||
|
constructor(
|
||||||
|
private readonly ignoredHeaders: boolean | string[],
|
||||||
|
private readonly setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
|
||||||
|
private readonly sanitize: (data: RequestResponseData) => RequestResponseData,
|
||||||
|
private readonly sendMessage: (item: NetworkRequest) => void,
|
||||||
|
private readonly isServiceUrl: (url: string) => boolean,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public apply(target: T, thisArg: T, argsList: any[]) {
|
||||||
|
const urlString: string = argsList[0]
|
||||||
|
const data: BodyInit = argsList[1]
|
||||||
|
const item = new NetworkMessage(this.ignoredHeaders, this.setSessionTokenHeader, this.sanitize)
|
||||||
|
if (this.isServiceUrl(urlString)) {
|
||||||
|
return target.apply(thisArg, argsList)
|
||||||
|
}
|
||||||
|
const url = getURL(urlString)
|
||||||
|
item.method = 'POST'
|
||||||
|
item.url = urlString
|
||||||
|
item.name = (url.pathname.split('/').pop() || '') + url.search
|
||||||
|
item.requestType = 'beacon'
|
||||||
|
item.requestHeader = { 'Content-Type': getContentType(data) }
|
||||||
|
item.status = 0
|
||||||
|
item.statusText = 'Pending'
|
||||||
|
|
||||||
|
if (url.search && url.searchParams) {
|
||||||
|
item.getData = {}
|
||||||
|
for (const [key, value] of url.searchParams) {
|
||||||
|
item.getData[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item.requestData = genStringBody(data)
|
||||||
|
|
||||||
|
if (!item.startTime) {
|
||||||
|
item.startTime = performance.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSuccess = target.apply(thisArg, argsList)
|
||||||
|
if (isSuccess) {
|
||||||
|
item.endTime = performance.now()
|
||||||
|
item.duration = item.endTime - (item.startTime || item.endTime)
|
||||||
|
item.status = 0
|
||||||
|
item.statusText = 'Sent'
|
||||||
|
item.readyState = 4
|
||||||
|
} else {
|
||||||
|
item.status = 500
|
||||||
|
item.statusText = 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendMessage(item.getMessage())
|
||||||
|
return isSuccess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class BeaconProxy {
|
||||||
|
public static origSendBeacon = window?.navigator?.sendBeacon
|
||||||
|
|
||||||
|
public static hasSendBeacon() {
|
||||||
|
return !!BeaconProxy.origSendBeacon
|
||||||
|
}
|
||||||
|
|
||||||
|
public static create(
|
||||||
|
ignoredHeaders: boolean | string[],
|
||||||
|
setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
|
||||||
|
sanitize: (data: RequestResponseData) => RequestResponseData,
|
||||||
|
sendMessage: (item: NetworkRequest) => void,
|
||||||
|
isServiceUrl: (url: string) => boolean,
|
||||||
|
) {
|
||||||
|
if (!BeaconProxy.hasSendBeacon()) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return new Proxy(
|
||||||
|
BeaconProxy.origSendBeacon,
|
||||||
|
new BeaconProxyHandler(
|
||||||
|
ignoredHeaders,
|
||||||
|
setSessionTokenHeader,
|
||||||
|
sanitize,
|
||||||
|
sendMessage,
|
||||||
|
isServiceUrl,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -291,8 +291,6 @@ export class FetchProxyHandler<T extends typeof fetch> implements ProxyHandler<T
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class FetchProxy {
|
export default class FetchProxy {
|
||||||
public static origFetch = fetch
|
|
||||||
|
|
||||||
public static create(
|
public static create(
|
||||||
ignoredHeaders: boolean | string[],
|
ignoredHeaders: boolean | string[],
|
||||||
setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
|
setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import FetchProxy from './fetchProxy.js'
|
import FetchProxy from './fetchProxy.js'
|
||||||
import XHRProxy from './xhrProxy.js'
|
import XHRProxy from './xhrProxy.js'
|
||||||
|
import BeaconProxy from './beaconProxy.js'
|
||||||
import { RequestResponseData } from './types.js'
|
import { RequestResponseData } from './types.js'
|
||||||
import { NetworkRequest } from '../../../common/messages.gen.js'
|
import { NetworkRequest } from '../../../common/messages.gen.js'
|
||||||
|
|
||||||
|
|
@ -40,4 +41,13 @@ export default function setProxy(
|
||||||
} else {
|
} else {
|
||||||
getWarning('fetch')
|
getWarning('fetch')
|
||||||
}
|
}
|
||||||
|
if (context?.navigator?.sendBeacon) {
|
||||||
|
context.navigator.sendBeacon = BeaconProxy.create(
|
||||||
|
ignoredHeaders,
|
||||||
|
setSessionTokenHeader,
|
||||||
|
sanitize,
|
||||||
|
sendMessage,
|
||||||
|
isServiceUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export default class NetworkMessage {
|
||||||
readyState?: RequestState = 0
|
readyState?: RequestState = 0
|
||||||
header: { [key: string]: string } = {}
|
header: { [key: string]: string } = {}
|
||||||
responseType: XMLHttpRequest['responseType'] = ''
|
responseType: XMLHttpRequest['responseType'] = ''
|
||||||
requestType: 'xhr' | 'fetch' | 'ping' | 'custom'
|
requestType: 'xhr' | 'fetch' | 'ping' | 'custom' | 'beacon'
|
||||||
requestHeader: HeadersInit = {}
|
requestHeader: HeadersInit = {}
|
||||||
response: any
|
response: any
|
||||||
responseSize = 0 // bytes
|
responseSize = 0 // bytes
|
||||||
|
|
@ -72,6 +72,7 @@ export default class NetworkMessage {
|
||||||
messageInfo.status,
|
messageInfo.status,
|
||||||
this.startTime + getTimeOrigin(),
|
this.startTime + getTimeOrigin(),
|
||||||
this.duration,
|
this.duration,
|
||||||
|
this.responseSize,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -226,8 +226,6 @@ export class XHRProxyHandler<T extends XMLHttpRequest> implements ProxyHandler<T
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class XHRProxy {
|
export default class XHRProxy {
|
||||||
public static origXMLHttpRequest = XMLHttpRequest
|
|
||||||
|
|
||||||
public static create(
|
public static create(
|
||||||
ignoredHeaders: boolean | string[],
|
ignoredHeaders: boolean | string[],
|
||||||
setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
|
setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ export default function (
|
||||||
stringify: (data: { headers: Record<string, string>; body: any }) => string,
|
stringify: (data: { headers: Record<string, string>; body: any }) => string,
|
||||||
) {
|
) {
|
||||||
app.debug.log('Openreplay: attaching axios spy to instance', instance)
|
app.debug.log('Openreplay: attaching axios spy to instance', instance)
|
||||||
|
|
||||||
function captureResponseData(axiosResponseObj: AxiosResponse) {
|
function captureResponseData(axiosResponseObj: AxiosResponse) {
|
||||||
app.debug.log('Openreplay: capturing axios response data', axiosResponseObj)
|
app.debug.log('Openreplay: capturing axios response data', axiosResponseObj)
|
||||||
const { headers: reqHs, data: reqData, method, url, baseURL } = axiosResponseObj.config
|
const { headers: reqHs, data: reqData, method, url, baseURL } = axiosResponseObj.config
|
||||||
|
|
@ -144,6 +145,7 @@ export default function (
|
||||||
reqResInfo.status,
|
reqResInfo.status,
|
||||||
requestStart + getTimeOrigin(),
|
requestStart + getTimeOrigin(),
|
||||||
duration,
|
duration,
|
||||||
|
0,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -183,6 +185,7 @@ export default function (
|
||||||
function logRequestError(ev: any) {
|
function logRequestError(ev: any) {
|
||||||
app.debug.log('Openreplay: failed API request, skipping', ev)
|
app.debug.log('Openreplay: failed API request, skipping', ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
const reqInt = instance.interceptors.request.use(getStartTime, logRequestError, {
|
const reqInt = instance.interceptors.request.use(getStartTime, logRequestError, {
|
||||||
synchronous: true,
|
synchronous: true,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,6 @@ export default class FeatureFlags {
|
||||||
userID: sessionInfo.userID,
|
userID: sessionInfo.userID,
|
||||||
metadata: sessionInfo.metadata,
|
metadata: sessionInfo.metadata,
|
||||||
referrer: document.referrer,
|
referrer: document.referrer,
|
||||||
// todo: get from backend
|
|
||||||
os: userInfo.userOS,
|
os: userInfo.userOS,
|
||||||
device: userInfo.userDevice,
|
device: userInfo.userDevice,
|
||||||
country: userInfo.userCountry,
|
country: userInfo.userCountry,
|
||||||
|
|
|
||||||
|
|
@ -222,6 +222,7 @@ export default function (app: App, opts: Partial<Options> = {}) {
|
||||||
r.status,
|
r.status,
|
||||||
startTime + getTimeOrigin(),
|
startTime + getTimeOrigin(),
|
||||||
duration,
|
duration,
|
||||||
|
0,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
@ -230,6 +231,7 @@ export default function (app: App, opts: Partial<Options> = {}) {
|
||||||
return response
|
return response
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// @ts-ignore
|
||||||
context.fetch = trackFetch
|
context.fetch = trackFetch
|
||||||
|
|
||||||
/* ====== <> ====== */
|
/* ====== <> ====== */
|
||||||
|
|
@ -293,6 +295,7 @@ export default function (app: App, opts: Partial<Options> = {}) {
|
||||||
xhr.status,
|
xhr.status,
|
||||||
startTime + getTimeOrigin(),
|
startTime + getTimeOrigin(),
|
||||||
duration,
|
duration,
|
||||||
|
0,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ describe('NetworkMessage', () => {
|
||||||
// yeah
|
// yeah
|
||||||
result[7],
|
result[7],
|
||||||
500,
|
500,
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
expect(result).toBeDefined()
|
expect(result).toBeDefined()
|
||||||
expect(result).toEqual(expected)
|
expect(result).toEqual(expected)
|
||||||
|
|
|
||||||
97
tracker/tracker/src/tests/networkProxy.test.ts
Normal file
97
tracker/tracker/src/tests/networkProxy.test.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
// @ts-nocheck
|
||||||
|
import { describe, it, expect, beforeEach, jest } from '@jest/globals'
|
||||||
|
import setProxy from '../main/modules/Network/index.js'
|
||||||
|
import FetchProxy from '../main/modules/Network/fetchProxy.js'
|
||||||
|
import XHRProxy from '../main/modules/Network/xhrProxy.js'
|
||||||
|
import BeaconProxy from '../main/modules/Network/beaconProxy.js'
|
||||||
|
|
||||||
|
globalThis.fetch = jest.fn()
|
||||||
|
|
||||||
|
jest.mock('../main/modules/Network/fetchProxy.js')
|
||||||
|
jest.mock('../main/modules/Network/xhrProxy.js')
|
||||||
|
jest.mock('../main/modules/Network/beaconProxy.js')
|
||||||
|
|
||||||
|
describe('Network Proxy', () => {
|
||||||
|
let context
|
||||||
|
const ignoredHeaders = []
|
||||||
|
const setSessionTokenHeader = jest.fn()
|
||||||
|
const sanitize = jest.fn()
|
||||||
|
const sendMessage = jest.fn()
|
||||||
|
const isServiceUrl = jest.fn()
|
||||||
|
const tokenUrlMatcher = jest.fn()
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
context = {
|
||||||
|
fetch: jest.fn(),
|
||||||
|
XMLHttpRequest: jest.fn(),
|
||||||
|
navigator: {
|
||||||
|
sendBeacon: jest.fn(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
FetchProxy.create.mockReturnValue(jest.fn())
|
||||||
|
XHRProxy.create.mockReturnValue(jest.fn())
|
||||||
|
BeaconProxy.create.mockReturnValue(jest.fn())
|
||||||
|
})
|
||||||
|
it('should not replace fetch if not present', () => {
|
||||||
|
context = {
|
||||||
|
XMLHttpRequest: jest.fn(),
|
||||||
|
navigator: {
|
||||||
|
sendBeacon: jest.fn(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
setProxy(
|
||||||
|
context,
|
||||||
|
ignoredHeaders,
|
||||||
|
setSessionTokenHeader,
|
||||||
|
sanitize,
|
||||||
|
sendMessage,
|
||||||
|
isServiceUrl,
|
||||||
|
tokenUrlMatcher,
|
||||||
|
)
|
||||||
|
expect(context.fetch).toBeUndefined()
|
||||||
|
expect(FetchProxy.create).toHaveBeenCalledTimes(0)
|
||||||
|
expect(XHRProxy.create).toHaveBeenCalled()
|
||||||
|
expect(BeaconProxy.create).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
it('should replace XMLHttpRequest if present', () => {
|
||||||
|
setProxy(
|
||||||
|
context,
|
||||||
|
ignoredHeaders,
|
||||||
|
setSessionTokenHeader,
|
||||||
|
sanitize,
|
||||||
|
sendMessage,
|
||||||
|
isServiceUrl,
|
||||||
|
tokenUrlMatcher,
|
||||||
|
)
|
||||||
|
expect(context.XMLHttpRequest).toEqual(expect.any(Function))
|
||||||
|
expect(XHRProxy.create).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should replace fetch if present', () => {
|
||||||
|
setProxy(
|
||||||
|
context,
|
||||||
|
ignoredHeaders,
|
||||||
|
setSessionTokenHeader,
|
||||||
|
sanitize,
|
||||||
|
sendMessage,
|
||||||
|
isServiceUrl,
|
||||||
|
tokenUrlMatcher,
|
||||||
|
)
|
||||||
|
expect(context.fetch).toEqual(expect.any(Function))
|
||||||
|
expect(FetchProxy.create).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should replace navigator.sendBeacon if present', () => {
|
||||||
|
setProxy(
|
||||||
|
context,
|
||||||
|
ignoredHeaders,
|
||||||
|
setSessionTokenHeader,
|
||||||
|
sanitize,
|
||||||
|
sendMessage,
|
||||||
|
isServiceUrl,
|
||||||
|
tokenUrlMatcher,
|
||||||
|
)
|
||||||
|
expect(context.navigator.sendBeacon).toEqual(expect.any(Function))
|
||||||
|
expect(BeaconProxy.create).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -78,7 +78,7 @@ export default class MessageEncoder extends PrimitiveEncoder {
|
||||||
return this.uint(msg[1]) && this.uint(msg[2])
|
return this.uint(msg[1]) && this.uint(msg[2])
|
||||||
break
|
break
|
||||||
|
|
||||||
case Messages.Type.NetworkRequest:
|
case Messages.Type.NetworkRequestDeprecated:
|
||||||
return this.string(msg[1]) && this.string(msg[2]) && this.string(msg[3]) && this.string(msg[4]) && this.string(msg[5]) && this.uint(msg[6]) && this.uint(msg[7]) && this.uint(msg[8])
|
return this.string(msg[1]) && this.string(msg[2]) && this.string(msg[3]) && this.string(msg[4]) && this.string(msg[5]) && this.uint(msg[6]) && this.uint(msg[7]) && this.uint(msg[8])
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
@ -254,6 +254,10 @@ export default class MessageEncoder extends PrimitiveEncoder {
|
||||||
return this.uint(msg[1]) && this.uint(msg[2])
|
return this.uint(msg[1]) && this.uint(msg[2])
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case Messages.Type.NetworkRequest:
|
||||||
|
return this.string(msg[1]) && this.string(msg[2]) && this.string(msg[3]) && this.string(msg[4]) && this.string(msg[5]) && this.uint(msg[6]) && this.uint(msg[7]) && this.uint(msg[8]) && this.uint(msg[9])
|
||||||
|
break
|
||||||
|
|
||||||
case Messages.Type.InputChange:
|
case Messages.Type.InputChange:
|
||||||
return this.uint(msg[1]) && this.string(msg[2]) && this.boolean(msg[3]) && this.string(msg[4]) && this.int(msg[5]) && this.int(msg[6])
|
return this.uint(msg[1]) && this.string(msg[2]) && this.boolean(msg[3]) && this.string(msg[4]) && this.int(msg[5]) && this.int(msg[6])
|
||||||
break
|
break
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue