feat: 新增trace功能

This commit is contained in:
weidingjian 2026-06-25 22:55:11 +08:00
parent c7e8e24332
commit 8c7e5947cd
89 changed files with 26880 additions and 97 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

224
coverage/base.css Normal file
View File

@ -0,0 +1,224 @@
body, html {
margin:0; padding: 0;
height: 100%;
}
body {
font-family: Helvetica Neue, Helvetica, Arial;
font-size: 14px;
color:#333;
}
.small { font-size: 12px; }
*, *:after, *:before {
-webkit-box-sizing:border-box;
-moz-box-sizing:border-box;
box-sizing:border-box;
}
h1 { font-size: 20px; margin: 0;}
h2 { font-size: 14px; }
pre {
font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
margin: 0;
padding: 0;
-moz-tab-size: 2;
-o-tab-size: 2;
tab-size: 2;
}
a { color:#0074D9; text-decoration:none; }
a:hover { text-decoration:underline; }
.strong { font-weight: bold; }
.space-top1 { padding: 10px 0 0 0; }
.pad2y { padding: 20px 0; }
.pad1y { padding: 10px 0; }
.pad2x { padding: 0 20px; }
.pad2 { padding: 20px; }
.pad1 { padding: 10px; }
.space-left2 { padding-left:55px; }
.space-right2 { padding-right:20px; }
.center { text-align:center; }
.clearfix { display:block; }
.clearfix:after {
content:'';
display:block;
height:0;
clear:both;
visibility:hidden;
}
.fl { float: left; }
@media only screen and (max-width:640px) {
.col3 { width:100%; max-width:100%; }
.hide-mobile { display:none!important; }
}
.quiet {
color: #7f7f7f;
color: rgba(0,0,0,0.5);
}
.quiet a { opacity: 0.7; }
.fraction {
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
font-size: 10px;
color: #555;
background: #E8E8E8;
padding: 4px 5px;
border-radius: 3px;
vertical-align: middle;
}
div.path a:link, div.path a:visited { color: #333; }
table.coverage {
border-collapse: collapse;
margin: 10px 0 0 0;
padding: 0;
}
table.coverage td {
margin: 0;
padding: 0;
vertical-align: top;
}
table.coverage td.line-count {
text-align: right;
padding: 0 5px 0 20px;
}
table.coverage td.line-coverage {
text-align: right;
padding-right: 10px;
min-width:20px;
}
table.coverage td span.cline-any {
display: inline-block;
padding: 0 5px;
width: 100%;
}
.missing-if-branch {
display: inline-block;
margin-right: 5px;
border-radius: 3px;
position: relative;
padding: 0 4px;
background: #333;
color: yellow;
}
.skip-if-branch {
display: none;
margin-right: 10px;
position: relative;
padding: 0 4px;
background: #ccc;
color: white;
}
.missing-if-branch .typ, .skip-if-branch .typ {
color: inherit !important;
}
.coverage-summary {
border-collapse: collapse;
width: 100%;
}
.coverage-summary tr { border-bottom: 1px solid #bbb; }
.keyline-all { border: 1px solid #ddd; }
.coverage-summary td, .coverage-summary th { padding: 10px; }
.coverage-summary tbody { border: 1px solid #bbb; }
.coverage-summary td { border-right: 1px solid #bbb; }
.coverage-summary td:last-child { border-right: none; }
.coverage-summary th {
text-align: left;
font-weight: normal;
white-space: nowrap;
}
.coverage-summary th.file { border-right: none !important; }
.coverage-summary th.pct { }
.coverage-summary th.pic,
.coverage-summary th.abs,
.coverage-summary td.pct,
.coverage-summary td.abs { text-align: right; }
.coverage-summary td.file { white-space: nowrap; }
.coverage-summary td.pic { min-width: 120px !important; }
.coverage-summary tfoot td { }
.coverage-summary .sorter {
height: 10px;
width: 7px;
display: inline-block;
margin-left: 0.5em;
background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
}
.coverage-summary .sorted .sorter {
background-position: 0 -20px;
}
.coverage-summary .sorted-desc .sorter {
background-position: 0 -10px;
}
.status-line { height: 10px; }
/* yellow */
.cbranch-no { background: yellow !important; color: #111; }
/* dark red */
.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
.low .chart { border:1px solid #C21F39 }
.highlighted,
.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
background: #C21F39 !important;
}
/* medium red */
.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
/* light red */
.low, .cline-no { background:#FCE1E5 }
/* light green */
.high, .cline-yes { background:rgb(230,245,208) }
/* medium green */
.cstat-yes { background:rgb(161,215,106) }
/* dark green */
.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
.high .chart { border:1px solid rgb(77,146,33) }
/* dark yellow (gold) */
.status-line.medium, .medium .cover-fill { background: #f9cd0b; }
.medium .chart { border:1px solid #f9cd0b; }
/* light yellow */
.medium { background: #fff4c2; }
.cstat-skip { background: #ddd; color: #111; }
.fstat-skip { background: #ddd; color: #111 !important; }
.cbranch-skip { background: #ddd !important; color: #111; }
span.cline-neutral { background: #eaeaea; }
.coverage-summary td.empty {
opacity: .5;
padding-top: 4px;
padding-bottom: 4px;
line-height: 1;
color: #888;
}
.cover-fill, .cover-empty {
display:inline-block;
height: 12px;
}
.chart {
line-height: 0;
}
.cover-empty {
background: white;
}
.cover-full {
border-right: none !important;
}
pre.prettyprint {
border: none !important;
padding: 0 !important;
margin: 0 !important;
}
.com { color: #999 !important; }
.ignore-none { color: #999; font-weight: normal; }
.wrapper {
min-height: 100%;
height: auto !important;
height: 100%;
margin: 0 auto -48px;
}
.footer, .push {
height: 48px;
}

View File

@ -0,0 +1,87 @@
/* eslint-disable */
var jumpToCode = (function init() {
// Classes of code we would like to highlight in the file view
var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
// Elements to highlight in the file listing view
var fileListingElements = ['td.pct.low'];
// We don't want to select elements that are direct descendants of another match
var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
// Selector that finds elements on the page to which we can jump
var selector =
fileListingElements.join(', ') +
', ' +
notSelector +
missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
// The NodeList of matching elements
var missingCoverageElements = document.querySelectorAll(selector);
var currentIndex;
function toggleClass(index) {
missingCoverageElements
.item(currentIndex)
.classList.remove('highlighted');
missingCoverageElements.item(index).classList.add('highlighted');
}
function makeCurrent(index) {
toggleClass(index);
currentIndex = index;
missingCoverageElements.item(index).scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
});
}
function goToPrevious() {
var nextIndex = 0;
if (typeof currentIndex !== 'number' || currentIndex === 0) {
nextIndex = missingCoverageElements.length - 1;
} else if (missingCoverageElements.length > 1) {
nextIndex = currentIndex - 1;
}
makeCurrent(nextIndex);
}
function goToNext() {
var nextIndex = 0;
if (
typeof currentIndex === 'number' &&
currentIndex < missingCoverageElements.length - 1
) {
nextIndex = currentIndex + 1;
}
makeCurrent(nextIndex);
}
return function jump(event) {
if (
document.getElementById('fileSearch') === document.activeElement &&
document.activeElement != null
) {
// if we're currently focused on the search input, we don't want to navigate
return;
}
switch (event.which) {
case 78: // n
case 74: // j
goToNext();
break;
case 66: // b
case 75: // k
case 80: // p
goToPrevious();
break;
}
};
})();
window.addEventListener('keydown', jumpToCode);

File diff suppressed because one or more lines are too long

BIN
coverage/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 B

161
coverage/index.html Normal file
View File

@ -0,0 +1,161 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for All files</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="prettify.css" />
<link rel="stylesheet" href="base.css" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1>All files</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">49.43% </span>
<span class="quiet">Statements</span>
<span class='fraction'>2635/5330</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">87.24% </span>
<span class="quiet">Branches</span>
<span class='fraction'>513/588</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">41.97% </span>
<span class="quiet">Functions</span>
<span class='fraction'>123/293</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">49.43% </span>
<span class="quiet">Lines</span>
<span class='fraction'>2635/5330</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line low'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody><tr>
<td class="file medium" data-value="src"><a href="src/index.html">src</a></td>
<td data-value="50.39" class="pic medium">
<div class="chart"><div class="cover-fill" style="width: 50%"></div><div class="cover-empty" style="width: 50%"></div></div>
</td>
<td data-value="50.39" class="pct medium">50.39%</td>
<td data-value="252" class="abs medium">127/252</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="0" class="abs high">0/0</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="21" class="abs low">0/21</td>
<td data-value="50.39" class="pct medium">50.39%</td>
<td data-value="252" class="abs medium">127/252</td>
</tr>
<tr>
<td class="file medium" data-value="src/core"><a href="src/core/index.html">src/core</a></td>
<td data-value="60.64" class="pic medium">
<div class="chart"><div class="cover-fill" style="width: 60%"></div><div class="cover-empty" style="width: 40%"></div></div>
</td>
<td data-value="60.64" class="pct medium">60.64%</td>
<td data-value="1606" class="abs medium">974/1606</td>
<td data-value="85.78" class="pct high">85.78%</td>
<td data-value="197" class="abs high">169/197</td>
<td data-value="52" class="pct medium">52%</td>
<td data-value="100" class="abs medium">52/100</td>
<td data-value="60.64" class="pct medium">60.64%</td>
<td data-value="1606" class="abs medium">974/1606</td>
</tr>
<tr>
<td class="file low" data-value="src/plugins"><a href="src/plugins/index.html">src/plugins</a></td>
<td data-value="18.25" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 18%"></div><div class="cover-empty" style="width: 82%"></div></div>
</td>
<td data-value="18.25" class="pct low">18.25%</td>
<td data-value="2334" class="abs low">426/2334</td>
<td data-value="89.28" class="pct high">89.28%</td>
<td data-value="28" class="abs high">25/28</td>
<td data-value="7.47" class="pct low">7.47%</td>
<td data-value="107" class="abs low">8/107</td>
<td data-value="18.25" class="pct low">18.25%</td>
<td data-value="2334" class="abs low">426/2334</td>
</tr>
<tr>
<td class="file high" data-value="src/utils"><a href="src/utils/index.html">src/utils</a></td>
<td data-value="97.36" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 97%"></div><div class="cover-empty" style="width: 3%"></div></div>
</td>
<td data-value="97.36" class="pct high">97.36%</td>
<td data-value="1138" class="abs high">1108/1138</td>
<td data-value="87.87" class="pct high">87.87%</td>
<td data-value="363" class="abs high">319/363</td>
<td data-value="96.92" class="pct high">96.92%</td>
<td data-value="65" class="abs high">63/65</td>
<td data-value="97.36" class="pct high">97.36%</td>
<td data-value="1138" class="abs high">1108/1138</td>
</tr>
</tbody>
</table>
</div>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-06-25T14:49:54.914Z
</div>
<script src="prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="sorter.js"></script>
<script src="block-navigation.js"></script>
</body>
</html>

1
coverage/prettify.css Normal file
View File

@ -0,0 +1 @@
.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}

2
coverage/prettify.js Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

210
coverage/sorter.js Normal file
View File

@ -0,0 +1,210 @@
/* eslint-disable */
var addSorting = (function() {
'use strict';
var cols,
currentSort = {
index: 0,
desc: false
};
// returns the summary table element
function getTable() {
return document.querySelector('.coverage-summary');
}
// returns the thead element of the summary table
function getTableHeader() {
return getTable().querySelector('thead tr');
}
// returns the tbody element of the summary table
function getTableBody() {
return getTable().querySelector('tbody');
}
// returns the th element for nth column
function getNthColumn(n) {
return getTableHeader().querySelectorAll('th')[n];
}
function onFilterInput() {
const searchValue = document.getElementById('fileSearch').value;
const rows = document.getElementsByTagName('tbody')[0].children;
// Try to create a RegExp from the searchValue. If it fails (invalid regex),
// it will be treated as a plain text search
let searchRegex;
try {
searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive
} catch (error) {
searchRegex = null;
}
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
let isMatch = false;
if (searchRegex) {
// If a valid regex was created, use it for matching
isMatch = searchRegex.test(row.textContent);
} else {
// Otherwise, fall back to the original plain text search
isMatch = row.textContent
.toLowerCase()
.includes(searchValue.toLowerCase());
}
row.style.display = isMatch ? '' : 'none';
}
}
// loads the search box
function addSearchBox() {
var template = document.getElementById('filterTemplate');
var templateClone = template.content.cloneNode(true);
templateClone.getElementById('fileSearch').oninput = onFilterInput;
template.parentElement.appendChild(templateClone);
}
// loads all columns
function loadColumns() {
var colNodes = getTableHeader().querySelectorAll('th'),
colNode,
cols = [],
col,
i;
for (i = 0; i < colNodes.length; i += 1) {
colNode = colNodes[i];
col = {
key: colNode.getAttribute('data-col'),
sortable: !colNode.getAttribute('data-nosort'),
type: colNode.getAttribute('data-type') || 'string'
};
cols.push(col);
if (col.sortable) {
col.defaultDescSort = col.type === 'number';
colNode.innerHTML =
colNode.innerHTML + '<span class="sorter"></span>';
}
}
return cols;
}
// attaches a data attribute to every tr element with an object
// of data values keyed by column name
function loadRowData(tableRow) {
var tableCols = tableRow.querySelectorAll('td'),
colNode,
col,
data = {},
i,
val;
for (i = 0; i < tableCols.length; i += 1) {
colNode = tableCols[i];
col = cols[i];
val = colNode.getAttribute('data-value');
if (col.type === 'number') {
val = Number(val);
}
data[col.key] = val;
}
return data;
}
// loads all row data
function loadData() {
var rows = getTableBody().querySelectorAll('tr'),
i;
for (i = 0; i < rows.length; i += 1) {
rows[i].data = loadRowData(rows[i]);
}
}
// sorts the table using the data for the ith column
function sortByIndex(index, desc) {
var key = cols[index].key,
sorter = function(a, b) {
a = a.data[key];
b = b.data[key];
return a < b ? -1 : a > b ? 1 : 0;
},
finalSorter = sorter,
tableBody = document.querySelector('.coverage-summary tbody'),
rowNodes = tableBody.querySelectorAll('tr'),
rows = [],
i;
if (desc) {
finalSorter = function(a, b) {
return -1 * sorter(a, b);
};
}
for (i = 0; i < rowNodes.length; i += 1) {
rows.push(rowNodes[i]);
tableBody.removeChild(rowNodes[i]);
}
rows.sort(finalSorter);
for (i = 0; i < rows.length; i += 1) {
tableBody.appendChild(rows[i]);
}
}
// removes sort indicators for current column being sorted
function removeSortIndicators() {
var col = getNthColumn(currentSort.index),
cls = col.className;
cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
col.className = cls;
}
// adds sort indicators for current column being sorted
function addSortIndicators() {
getNthColumn(currentSort.index).className += currentSort.desc
? ' sorted-desc'
: ' sorted';
}
// adds event listeners for all sorter widgets
function enableUI() {
var i,
el,
ithSorter = function ithSorter(i) {
var col = cols[i];
return function() {
var desc = col.defaultDescSort;
if (currentSort.index === i) {
desc = !currentSort.desc;
}
sortByIndex(i, desc);
removeSortIndicators();
currentSort.index = i;
currentSort.desc = desc;
addSortIndicators();
};
};
for (i = 0; i < cols.length; i += 1) {
if (cols[i].sortable) {
// add the click event handler on the th so users
// dont have to click on those tiny arrows
el = getNthColumn(i).querySelector('.sorter').parentElement;
if (el.addEventListener) {
el.addEventListener('click', ithSorter(i));
} else {
el.attachEvent('onclick', ithSorter(i));
}
}
}
}
// adds sorting functionality to the UI
return function() {
if (!getTable()) {
return;
}
cols = loadColumns();
loadData();
addSearchBox();
addSortIndicators();
enableUI();
};
})();
window.addEventListener('load', addSorting);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,817 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for src/core/ConfigManager.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../../index.html">All files</a> / <a href="index.html">src/core</a> ConfigManager.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">95.9% </span>
<span class="quiet">Statements</span>
<span class='fraction'>234/244</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">81.81% </span>
<span class="quiet">Branches</span>
<span class='fraction'>45/55</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">92.85% </span>
<span class="quiet">Functions</span>
<span class='fraction'>13/14</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">95.9% </span>
<span class="quiet">Lines</span>
<span class='fraction'>234/244</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a>
<a name='L103'></a><a href='#L103'>103</a>
<a name='L104'></a><a href='#L104'>104</a>
<a name='L105'></a><a href='#L105'>105</a>
<a name='L106'></a><a href='#L106'>106</a>
<a name='L107'></a><a href='#L107'>107</a>
<a name='L108'></a><a href='#L108'>108</a>
<a name='L109'></a><a href='#L109'>109</a>
<a name='L110'></a><a href='#L110'>110</a>
<a name='L111'></a><a href='#L111'>111</a>
<a name='L112'></a><a href='#L112'>112</a>
<a name='L113'></a><a href='#L113'>113</a>
<a name='L114'></a><a href='#L114'>114</a>
<a name='L115'></a><a href='#L115'>115</a>
<a name='L116'></a><a href='#L116'>116</a>
<a name='L117'></a><a href='#L117'>117</a>
<a name='L118'></a><a href='#L118'>118</a>
<a name='L119'></a><a href='#L119'>119</a>
<a name='L120'></a><a href='#L120'>120</a>
<a name='L121'></a><a href='#L121'>121</a>
<a name='L122'></a><a href='#L122'>122</a>
<a name='L123'></a><a href='#L123'>123</a>
<a name='L124'></a><a href='#L124'>124</a>
<a name='L125'></a><a href='#L125'>125</a>
<a name='L126'></a><a href='#L126'>126</a>
<a name='L127'></a><a href='#L127'>127</a>
<a name='L128'></a><a href='#L128'>128</a>
<a name='L129'></a><a href='#L129'>129</a>
<a name='L130'></a><a href='#L130'>130</a>
<a name='L131'></a><a href='#L131'>131</a>
<a name='L132'></a><a href='#L132'>132</a>
<a name='L133'></a><a href='#L133'>133</a>
<a name='L134'></a><a href='#L134'>134</a>
<a name='L135'></a><a href='#L135'>135</a>
<a name='L136'></a><a href='#L136'>136</a>
<a name='L137'></a><a href='#L137'>137</a>
<a name='L138'></a><a href='#L138'>138</a>
<a name='L139'></a><a href='#L139'>139</a>
<a name='L140'></a><a href='#L140'>140</a>
<a name='L141'></a><a href='#L141'>141</a>
<a name='L142'></a><a href='#L142'>142</a>
<a name='L143'></a><a href='#L143'>143</a>
<a name='L144'></a><a href='#L144'>144</a>
<a name='L145'></a><a href='#L145'>145</a>
<a name='L146'></a><a href='#L146'>146</a>
<a name='L147'></a><a href='#L147'>147</a>
<a name='L148'></a><a href='#L148'>148</a>
<a name='L149'></a><a href='#L149'>149</a>
<a name='L150'></a><a href='#L150'>150</a>
<a name='L151'></a><a href='#L151'>151</a>
<a name='L152'></a><a href='#L152'>152</a>
<a name='L153'></a><a href='#L153'>153</a>
<a name='L154'></a><a href='#L154'>154</a>
<a name='L155'></a><a href='#L155'>155</a>
<a name='L156'></a><a href='#L156'>156</a>
<a name='L157'></a><a href='#L157'>157</a>
<a name='L158'></a><a href='#L158'>158</a>
<a name='L159'></a><a href='#L159'>159</a>
<a name='L160'></a><a href='#L160'>160</a>
<a name='L161'></a><a href='#L161'>161</a>
<a name='L162'></a><a href='#L162'>162</a>
<a name='L163'></a><a href='#L163'>163</a>
<a name='L164'></a><a href='#L164'>164</a>
<a name='L165'></a><a href='#L165'>165</a>
<a name='L166'></a><a href='#L166'>166</a>
<a name='L167'></a><a href='#L167'>167</a>
<a name='L168'></a><a href='#L168'>168</a>
<a name='L169'></a><a href='#L169'>169</a>
<a name='L170'></a><a href='#L170'>170</a>
<a name='L171'></a><a href='#L171'>171</a>
<a name='L172'></a><a href='#L172'>172</a>
<a name='L173'></a><a href='#L173'>173</a>
<a name='L174'></a><a href='#L174'>174</a>
<a name='L175'></a><a href='#L175'>175</a>
<a name='L176'></a><a href='#L176'>176</a>
<a name='L177'></a><a href='#L177'>177</a>
<a name='L178'></a><a href='#L178'>178</a>
<a name='L179'></a><a href='#L179'>179</a>
<a name='L180'></a><a href='#L180'>180</a>
<a name='L181'></a><a href='#L181'>181</a>
<a name='L182'></a><a href='#L182'>182</a>
<a name='L183'></a><a href='#L183'>183</a>
<a name='L184'></a><a href='#L184'>184</a>
<a name='L185'></a><a href='#L185'>185</a>
<a name='L186'></a><a href='#L186'>186</a>
<a name='L187'></a><a href='#L187'>187</a>
<a name='L188'></a><a href='#L188'>188</a>
<a name='L189'></a><a href='#L189'>189</a>
<a name='L190'></a><a href='#L190'>190</a>
<a name='L191'></a><a href='#L191'>191</a>
<a name='L192'></a><a href='#L192'>192</a>
<a name='L193'></a><a href='#L193'>193</a>
<a name='L194'></a><a href='#L194'>194</a>
<a name='L195'></a><a href='#L195'>195</a>
<a name='L196'></a><a href='#L196'>196</a>
<a name='L197'></a><a href='#L197'>197</a>
<a name='L198'></a><a href='#L198'>198</a>
<a name='L199'></a><a href='#L199'>199</a>
<a name='L200'></a><a href='#L200'>200</a>
<a name='L201'></a><a href='#L201'>201</a>
<a name='L202'></a><a href='#L202'>202</a>
<a name='L203'></a><a href='#L203'>203</a>
<a name='L204'></a><a href='#L204'>204</a>
<a name='L205'></a><a href='#L205'>205</a>
<a name='L206'></a><a href='#L206'>206</a>
<a name='L207'></a><a href='#L207'>207</a>
<a name='L208'></a><a href='#L208'>208</a>
<a name='L209'></a><a href='#L209'>209</a>
<a name='L210'></a><a href='#L210'>210</a>
<a name='L211'></a><a href='#L211'>211</a>
<a name='L212'></a><a href='#L212'>212</a>
<a name='L213'></a><a href='#L213'>213</a>
<a name='L214'></a><a href='#L214'>214</a>
<a name='L215'></a><a href='#L215'>215</a>
<a name='L216'></a><a href='#L216'>216</a>
<a name='L217'></a><a href='#L217'>217</a>
<a name='L218'></a><a href='#L218'>218</a>
<a name='L219'></a><a href='#L219'>219</a>
<a name='L220'></a><a href='#L220'>220</a>
<a name='L221'></a><a href='#L221'>221</a>
<a name='L222'></a><a href='#L222'>222</a>
<a name='L223'></a><a href='#L223'>223</a>
<a name='L224'></a><a href='#L224'>224</a>
<a name='L225'></a><a href='#L225'>225</a>
<a name='L226'></a><a href='#L226'>226</a>
<a name='L227'></a><a href='#L227'>227</a>
<a name='L228'></a><a href='#L228'>228</a>
<a name='L229'></a><a href='#L229'>229</a>
<a name='L230'></a><a href='#L230'>230</a>
<a name='L231'></a><a href='#L231'>231</a>
<a name='L232'></a><a href='#L232'>232</a>
<a name='L233'></a><a href='#L233'>233</a>
<a name='L234'></a><a href='#L234'>234</a>
<a name='L235'></a><a href='#L235'>235</a>
<a name='L236'></a><a href='#L236'>236</a>
<a name='L237'></a><a href='#L237'>237</a>
<a name='L238'></a><a href='#L238'>238</a>
<a name='L239'></a><a href='#L239'>239</a>
<a name='L240'></a><a href='#L240'>240</a>
<a name='L241'></a><a href='#L241'>241</a>
<a name='L242'></a><a href='#L242'>242</a>
<a name='L243'></a><a href='#L243'>243</a>
<a name='L244'></a><a href='#L244'>244</a>
<a name='L245'></a><a href='#L245'>245</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">40x</span>
<span class="cline-any cline-yes">40x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import type { LightConfig, SentryEvent, UserInfo, ContextLevelMap, ErrorEvent, StackFrame, Breadcrumb, EventLevel } from '../types';
import { getContexts, getContextId, getPageUrl, getReferrer } from '../utils/env';
import { matchPatterns } from '../utils/helper';
import { getAnonymousId, isAnonymousTrackingEnabled } from '../utils/identity';
&nbsp;
const DEFAULT_CONFIG: Partial&lt;LightConfig&gt; = {
enabled: true,
sampleRate: 1,
maxQueueSize: 100,
flushInterval: 5000,
maxRetries: 3,
retryDelay: 1000,
environment: 'production',
// 上下文分层策略:按事件级别决定堆栈深度和 breadcrumbs 数量
contextLevel: {
fatal: { maxStackFrames: 5, maxBreadcrumbs: 20 },
error: { maxStackFrames: 5, maxBreadcrumbs: 10 },
warning: { maxStackFrames: 3, maxBreadcrumbs: 5 },
info: { maxStackFrames: 0, maxBreadcrumbs: 0 },
debug: { maxStackFrames: 0, maxBreadcrumbs: 0 },
},
};
&nbsp;
export class ConfigManager {
private config: LightConfig;
private contexts: ReturnType&lt;typeof getContexts&gt; | null = null;
private contextId: string | null = null;
private contextReported: boolean = false;
&nbsp;
constructor(config: LightConfig) {
this.config = { ...DEFAULT_CONFIG, ...config } as LightConfig;
}
&nbsp;
markContextReported(): void {
this.contextReported = true;
}
&nbsp;
get&lt;K extends keyof LightConfig&gt;(key: K): LightConfig[K] {
return this.config[key];
}
&nbsp;
getAll(): LightConfig {
return { ...this.config };
}
&nbsp;
set&lt;K extends keyof LightConfig&gt;(key: K, value: LightConfig[K]): void {
this.config[key] = value;
}
&nbsp;
setUser(user: UserInfo | null): void {
this.config.user = user || undefined;
}
&nbsp;
setTags(tags: Record&lt;string, string&gt;): void {
this.config.tags = { ...(this.config.tags || {}), ...tags };
}
&nbsp;
setTag(key: string, value: string): void {
if (!this.config.tags) {
this.config.tags = {};
}
(this.config.tags as Record&lt;string, string&gt;)[key] = value;
}
&nbsp;
setExtra(key: string, value: unknown): void {
if (!this.config.extra) {
this.config.extra = {};
}
(this.config.extra as Record&lt;string, unknown&gt;)[key] = value;
}
&nbsp;
isIgnoredError(message: string): boolean {
return matchPatterns(message, this.config.<span class="branch-0 cbranch-no" title="branch not covered" >ignoreErrors || [])</span>;
}
&nbsp;
shouldSample(): boolean {
const rate = this.config.<span class="branch-0 cbranch-no" title="branch not covered" >sampleRate ?? 1;</span>
if (rate &gt;= 1) return true;
if (rate &lt;= 0) return <span class="branch-0 cbranch-no" title="branch not covered" >false;</span>
<span class="cstat-no" title="statement not covered" > return Math.random() &lt; rate;</span>
}
&nbsp;
/**
* 获取编码后的环境信息(延迟初始化,只在首次上报时计算)
* 注意env.ts 的 getContexts 已经返回编码后的数据
*/
private <span class="fstat-no" title="function not covered" >getEncodedContext(): ReturnType&lt;typeof getContexts&gt; {</span>
<span class="cstat-no" title="statement not covered" > if (!this.contexts) {</span>
<span class="cstat-no" title="statement not covered" > this.contexts = getContexts();</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > return this.contexts;</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
/**
* 将配置应用到事件
* @param event 事件对象
* @param includeFullContext 是否包含完整环境信息(批次内只有首个事件需要)
*/
applyToEvent(event: SentryEvent, includeFullContext: boolean = true): SentryEvent {
// 使用 Record 来避免联合类型的问题
const enriched: Record&lt;string, unknown&gt; = { ...event };
&nbsp;
// 处理 tags
if (event.tags) {
enriched.tags = { ...event.tags };
}
&nbsp;
// 处理 request
if (event.request) {
enriched.request = { ...event.request };
}
&nbsp;
// 处理 exception仅 error 事件有)
if ('exception' in event &amp;&amp; event.exception) {
const exc = event.exception;
enriched.exception = {
...exc,
stacktrace: exc.stacktrace ? {
...exc.stacktrace,
frames: exc.stacktrace.frames ? exc.stacktrace.frames.map((f: StackFrame) =&gt; ({ ...f })<span class="branch-0 cbranch-no" title="branch not covered" >) : undefined,</span>
<span class="branch-0 cbranch-no" title="branch not covered" > } : undefined,</span>
};
}
&nbsp;
// 处理 breadcrumbs
if (event.breadcrumbs) {
enriched.breadcrumbs = event.breadcrumbs.map((b: Breadcrumb) =&gt; ({ ...b }));
}
&nbsp;
// 处理 user
if (event.user) {
enriched.user = { ...event.user };
}
&nbsp;
// 处理 contexts
if (event.contexts) <span class="branch-0 cbranch-no" title="branch not covered" >{</span>
<span class="cstat-no" title="statement not covered" > enriched.contexts = { ...event.contexts };</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
if (this.config.release) {
enriched.release = this.config.release;
}
if (this.config.environment) {
enriched.environment = this.config.environment;
}
enriched.platform = 'javascript';
if (this.config.user) {
enriched.user = this.config.user;
}
if (this.config.tags) {
enriched.tags = { ...this.config.tags, ...(event.tags as Record&lt;string, string&gt; || {}) };
}
&nbsp;
if (!enriched.request) {
enriched.request = {
url: getPageUrl(),
referrer: getReferrer(),
};
} else if (!(enriched.request as Record&lt;string, unknown&gt;).url) <span class="branch-0 cbranch-no" title="branch not covered" >{</span>
<span class="cstat-no" title="statement not covered" > (enriched.request as Record&lt;string, unknown&gt;).url = getPageUrl();</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
if (!this.contextId) {
this.contextId = getContextId();
}
enriched.context_id = this.contextId;
&nbsp;
// 处理匿名用户ID
// 只有在没有设置 user.id 且启用了匿名追踪时,才添加 anonymousId
const hasUserId = this.config.user?.id || (event.user as Record&lt;string, unknown&gt;)?.id;
if (!hasUserId &amp;&amp; isAnonymousTrackingEnabled(this.config)) {
const anonymousId = getAnonymousId();
if (anonymousId) {
enriched.anonymousId = anonymousId;
}
}
&nbsp;
const result = this.applyContextLevel(enriched);
&nbsp;
if (includeFullContext &amp;&amp; !this.contextReported) {
if (!this.contexts) {
this.contexts = getContexts();
}
(result as Record&lt;string, unknown&gt;).contexts = this.contexts;
(result as Record&lt;string, unknown&gt;).env = {
b: this.contexts.browser?.name,
bv: this.contexts.browser?.version,
os: this.contexts.os?.name,
osv: this.contexts.os?.version,
};
}
&nbsp;
return result as unknown as SentryEvent;
}
&nbsp;
/**
* 应用上下文分层策略:根据事件级别裁剪堆栈和 breadcrumbs
* 注意:此方法不修改原始 event 对象,返回裁剪后的副本
*/
private applyContextLevel(event: Record&lt;string, unknown&gt;): Record&lt;string, unknown&gt; {
const contextLevel = this.config.contextLevel as ContextLevelMap | undefined;
if (!contextLevel) <span class="branch-0 cbranch-no" title="branch not covered" >return event;</span>
&nbsp;
const level = (event.<span class="branch-0 cbranch-no" title="branch not covered" >level || 'error') as EventLevel;</span>
const levelConfig = contextLevel[level<span class="branch-0 cbranch-no" title="branch not covered" >] || contextLevel['error'];</span>
&nbsp;
// 创建浅拷贝用于裁剪
const enriched: Record&lt;string, unknown&gt; = { ...event };
&nbsp;
// 裁剪堆栈帧(仅 error 事件有 exception
if ('exception' in event &amp;&amp; event.exception &amp;&amp; typeof event.exception === 'object') {
const exc = event.exception as Record&lt;string, unknown&gt;;
if (exc.stacktrace &amp;&amp; typeof exc.stacktrace === 'object') {
const st = exc.stacktrace as Record&lt;string, unknown&gt;;
if (Array.isArray(st.frames)) {
if (levelConfig.maxStackFrames === 0) {
// info/debug 级别不需要堆栈
delete enriched.exception;
} else {
enriched.exception = {
...exc,
stacktrace: {
...st,
frames: (st.frames as StackFrame[]).slice(0, levelConfig.maxStackFrames),
},
};
}
}
}
}
&nbsp;
// 裁剪 breadcrumbs
if (event.breadcrumbs &amp;&amp; Array.isArray(event.breadcrumbs)) {
if (levelConfig.maxBreadcrumbs === 0) {
// info/debug 级别不需要 breadcrumbs
delete enriched.breadcrumbs;
} else {
enriched.breadcrumbs = (event.breadcrumbs as Breadcrumb[]).slice(-levelConfig.maxBreadcrumbs);
}
}
&nbsp;
return enriched;
}
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-06-25T14:49:54.914Z
</div>
<script src="../../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../../sorter.js"></script>
<script src="../../block-navigation.js"></script>
</body>
</html>

View File

@ -0,0 +1,226 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for src/core/EventBus.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../../index.html">All files</a> / <a href="index.html">src/core</a> EventBus.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>47/47</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>13/13</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>7/7</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>47/47</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">42x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">88x</span>
<span class="cline-any cline-yes">88x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">88x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">type Handler = (...args: unknown[]) =&gt; void;
&nbsp;
export class EventBus {
private handlers: Map&lt;string, Handler[]&gt; = new Map();
&nbsp;
on(event: string, handler: Handler): void {
if (!this.handlers.has(event)) {
this.handlers.set(event, []);
}
this.handlers.get(event)!.push(handler);
}
&nbsp;
off(event: string, handler: Handler): void {
const eventHandlers = this.handlers.get(event);
if (eventHandlers) {
const index = eventHandlers.indexOf(handler);
if (index &gt; -1) {
eventHandlers.splice(index, 1);
}
}
}
&nbsp;
emit(event: string, ...args: unknown[]): void {
const eventHandlers = this.handlers.get(event);
if (eventHandlers) {
for (const handler of eventHandlers) {
try {
handler(...args);
} catch (e) {
console.error('[LightSDK] EventBus handler error:', e);
}
}
}
}
&nbsp;
once(event: string, handler: Handler): void {
const wrapped = (...args: unknown[]) =&gt; {
this.off(event, wrapped);
handler(...args);
};
this.on(event, wrapped);
}
&nbsp;
destroy(): void {
this.handlers.clear();
}
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-06-25T14:49:54.914Z
</div>
<script src="../../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../../sorter.js"></script>
<script src="../../block-navigation.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,346 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for src/core/PluginManager.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../../index.html">All files</a> / <a href="index.html">src/core</a> PluginManager.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>87/87</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>26/26</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>8/8</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>87/87</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">30x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">29x</span>
<span class="cline-any cline-yes">29x</span>
<span class="cline-any cline-yes">29x</span>
<span class="cline-any cline-yes">30x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">30x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import type { LightPlugin, LightClient, SentryEvent } from '../types';
&nbsp;
export class PluginManager {
private plugins: Map&lt;string, LightPlugin&gt; = new Map();
private client: LightClient;
&nbsp;
constructor(client: LightClient) {
this.client = client;
}
&nbsp;
add(plugin: LightPlugin): void {
if (this.plugins.has(plugin.name)) {
return;
}
this.plugins.set(plugin.name, plugin);
try {
plugin.setup(this.client);
} catch (e) {
console.error(`[LightSDK] Plugin "${plugin.name}" setup error:`, e);
}
}
&nbsp;
remove(name: string): void {
const plugin = this.plugins.get(name);
if (plugin) {
if (plugin.destroy) {
try {
plugin.destroy();
} catch (e) {
console.error(`[LightSDK] Plugin "${name}" destroy error:`, e);
}
}
this.plugins.delete(name);
}
}
&nbsp;
get(name: string): LightPlugin | undefined {
return this.plugins.get(name);
}
&nbsp;
has(name: string): boolean {
return this.plugins.has(name);
}
&nbsp;
applyBeforeReport(event: SentryEvent): SentryEvent | null {
let result: SentryEvent | null = event;
for (const plugin of this.plugins.values()) {
if (plugin.beforeReport) {
try {
const modified = plugin.beforeReport(result);
if (modified === null) {
return null;
}
result = modified;
} catch (e) {
console.error(`[LightSDK] Plugin "${plugin.name}" beforeReport error:`, e);
}
}
}
return result;
}
&nbsp;
applyAfterReport(event: SentryEvent): void {
for (const plugin of this.plugins.values()) {
if (plugin.afterReport) {
try {
plugin.afterReport(event);
} catch (e) {
console.error(`[LightSDK] Plugin "${plugin.name}" afterReport error:`, e);
}
}
}
}
&nbsp;
destroy(): void {
for (const plugin of this.plugins.values()) {
if (plugin.destroy) {
try {
plugin.destroy();
} catch (e) {
console.error(`[LightSDK] Plugin "${plugin.name}" destroy error:`, e);
}
}
}
this.plugins.clear();
}
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-06-25T14:49:54.914Z
</div>
<script src="../../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../../sorter.js"></script>
<script src="../../block-navigation.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,679 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for src/core/TracingManager.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../../index.html">All files</a> / <a href="index.html">src/core</a> TracingManager.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">86.86% </span>
<span class="quiet">Statements</span>
<span class='fraction'>172/198</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">80% </span>
<span class="quiet">Branches</span>
<span class='fraction'>28/35</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">78.57% </span>
<span class="quiet">Functions</span>
<span class='fraction'>11/14</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">86.86% </span>
<span class="quiet">Lines</span>
<span class='fraction'>172/198</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a>
<a name='L103'></a><a href='#L103'>103</a>
<a name='L104'></a><a href='#L104'>104</a>
<a name='L105'></a><a href='#L105'>105</a>
<a name='L106'></a><a href='#L106'>106</a>
<a name='L107'></a><a href='#L107'>107</a>
<a name='L108'></a><a href='#L108'>108</a>
<a name='L109'></a><a href='#L109'>109</a>
<a name='L110'></a><a href='#L110'>110</a>
<a name='L111'></a><a href='#L111'>111</a>
<a name='L112'></a><a href='#L112'>112</a>
<a name='L113'></a><a href='#L113'>113</a>
<a name='L114'></a><a href='#L114'>114</a>
<a name='L115'></a><a href='#L115'>115</a>
<a name='L116'></a><a href='#L116'>116</a>
<a name='L117'></a><a href='#L117'>117</a>
<a name='L118'></a><a href='#L118'>118</a>
<a name='L119'></a><a href='#L119'>119</a>
<a name='L120'></a><a href='#L120'>120</a>
<a name='L121'></a><a href='#L121'>121</a>
<a name='L122'></a><a href='#L122'>122</a>
<a name='L123'></a><a href='#L123'>123</a>
<a name='L124'></a><a href='#L124'>124</a>
<a name='L125'></a><a href='#L125'>125</a>
<a name='L126'></a><a href='#L126'>126</a>
<a name='L127'></a><a href='#L127'>127</a>
<a name='L128'></a><a href='#L128'>128</a>
<a name='L129'></a><a href='#L129'>129</a>
<a name='L130'></a><a href='#L130'>130</a>
<a name='L131'></a><a href='#L131'>131</a>
<a name='L132'></a><a href='#L132'>132</a>
<a name='L133'></a><a href='#L133'>133</a>
<a name='L134'></a><a href='#L134'>134</a>
<a name='L135'></a><a href='#L135'>135</a>
<a name='L136'></a><a href='#L136'>136</a>
<a name='L137'></a><a href='#L137'>137</a>
<a name='L138'></a><a href='#L138'>138</a>
<a name='L139'></a><a href='#L139'>139</a>
<a name='L140'></a><a href='#L140'>140</a>
<a name='L141'></a><a href='#L141'>141</a>
<a name='L142'></a><a href='#L142'>142</a>
<a name='L143'></a><a href='#L143'>143</a>
<a name='L144'></a><a href='#L144'>144</a>
<a name='L145'></a><a href='#L145'>145</a>
<a name='L146'></a><a href='#L146'>146</a>
<a name='L147'></a><a href='#L147'>147</a>
<a name='L148'></a><a href='#L148'>148</a>
<a name='L149'></a><a href='#L149'>149</a>
<a name='L150'></a><a href='#L150'>150</a>
<a name='L151'></a><a href='#L151'>151</a>
<a name='L152'></a><a href='#L152'>152</a>
<a name='L153'></a><a href='#L153'>153</a>
<a name='L154'></a><a href='#L154'>154</a>
<a name='L155'></a><a href='#L155'>155</a>
<a name='L156'></a><a href='#L156'>156</a>
<a name='L157'></a><a href='#L157'>157</a>
<a name='L158'></a><a href='#L158'>158</a>
<a name='L159'></a><a href='#L159'>159</a>
<a name='L160'></a><a href='#L160'>160</a>
<a name='L161'></a><a href='#L161'>161</a>
<a name='L162'></a><a href='#L162'>162</a>
<a name='L163'></a><a href='#L163'>163</a>
<a name='L164'></a><a href='#L164'>164</a>
<a name='L165'></a><a href='#L165'>165</a>
<a name='L166'></a><a href='#L166'>166</a>
<a name='L167'></a><a href='#L167'>167</a>
<a name='L168'></a><a href='#L168'>168</a>
<a name='L169'></a><a href='#L169'>169</a>
<a name='L170'></a><a href='#L170'>170</a>
<a name='L171'></a><a href='#L171'>171</a>
<a name='L172'></a><a href='#L172'>172</a>
<a name='L173'></a><a href='#L173'>173</a>
<a name='L174'></a><a href='#L174'>174</a>
<a name='L175'></a><a href='#L175'>175</a>
<a name='L176'></a><a href='#L176'>176</a>
<a name='L177'></a><a href='#L177'>177</a>
<a name='L178'></a><a href='#L178'>178</a>
<a name='L179'></a><a href='#L179'>179</a>
<a name='L180'></a><a href='#L180'>180</a>
<a name='L181'></a><a href='#L181'>181</a>
<a name='L182'></a><a href='#L182'>182</a>
<a name='L183'></a><a href='#L183'>183</a>
<a name='L184'></a><a href='#L184'>184</a>
<a name='L185'></a><a href='#L185'>185</a>
<a name='L186'></a><a href='#L186'>186</a>
<a name='L187'></a><a href='#L187'>187</a>
<a name='L188'></a><a href='#L188'>188</a>
<a name='L189'></a><a href='#L189'>189</a>
<a name='L190'></a><a href='#L190'>190</a>
<a name='L191'></a><a href='#L191'>191</a>
<a name='L192'></a><a href='#L192'>192</a>
<a name='L193'></a><a href='#L193'>193</a>
<a name='L194'></a><a href='#L194'>194</a>
<a name='L195'></a><a href='#L195'>195</a>
<a name='L196'></a><a href='#L196'>196</a>
<a name='L197'></a><a href='#L197'>197</a>
<a name='L198'></a><a href='#L198'>198</a>
<a name='L199'></a><a href='#L199'>199</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">29x</span>
<span class="cline-any cline-yes">29x</span>
<span class="cline-any cline-yes">29x</span>
<span class="cline-any cline-yes">29x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">17x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">17x</span>
<span class="cline-any cline-yes">17x</span>
<span class="cline-any cline-yes">17x</span>
<span class="cline-any cline-yes">17x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">17x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import type { LightConfig, SpanContext, TransactionContext, Span, Transaction, LightClient, SamplingContext } from '../types';
import { Span as SpanImpl, Transaction as TransactionImpl } from '../utils/tracing';
import { logDebug } from '../utils/logger';
&nbsp;
export class TracingManager {
private config: LightConfig;
private _client?: LightClient;
private currentTransaction: Transaction | null = null;
private spanStack: Span[] = [];
private dsnPublicKey?: string;
&nbsp;
constructor(config: LightConfig, client?: LightClient, dsnPublicKey?: string) {
this.config = config;
this._client = client;
this.dsnPublicKey = dsnPublicKey;
}
&nbsp;
updateConfig(config: LightConfig): void {
this.config = config;
if (this.currentTransaction) {
const txImpl = this.currentTransaction as unknown as TransactionImpl;
txImpl.setUserBaggage(config.user?.id, (config.user as { segment?: string } | undefined)?.segment);
}
}
&nbsp;
private shouldSample(context: TransactionContext, parentSampled?: boolean): boolean {
const sampler = this.config.tracesSampler;
if (sampler) {
const samplingContext: SamplingContext = {
...context,
parentSampled,
};
const result = sampler(samplingContext);
if (typeof result === 'boolean') {
return result;
<span class="branch-0 cbranch-no" title="branch not covered" > }</span>
<span class="cstat-no" title="statement not covered" > const clampedRate = Math.max(0, Math.min(1, result));</span>
<span class="cstat-no" title="statement not covered" > if (clampedRate === 0) return false;</span>
<span class="cstat-no" title="statement not covered" > if (clampedRate === 1) return true;</span>
<span class="cstat-no" title="statement not covered" > return Math.random() &lt; clampedRate;</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
const rate = this.config.<span class="branch-0 cbranch-no" title="branch not covered" >tracesSampleRate ?? 0;</span>
if (rate &gt;= 1) return true;
if (rate &lt;= 0) return <span class="branch-0 cbranch-no" title="branch not covered" >false;</span>
<span class="cstat-no" title="statement not covered" > return Math.random() &lt; rate;</span>
}
&nbsp;
startTransaction(context: TransactionContext): Transaction | null {
const parentTx = this.currentTransaction;
const parentSampled = parentTx ? parentTx.sampled : undefined;
&nbsp;
const sampled = context.sampled ?? this.shouldSample(context, parentSampled);
&nbsp;
const transaction = new TransactionImpl(
{
...context,
sampled,
},
this._client,
this.dsnPublicKey,
this.config.release,
this.config.environment
) as unknown as Transaction;
&nbsp;
this.currentTransaction = transaction;
this.spanStack = [transaction];
&nbsp;
logDebug('TracingManager', 'Transaction started', {
name: transaction.name,
traceId: transaction.traceId,
spanId: transaction.spanId,
sampled,
op: transaction.op,
});
&nbsp;
return transaction;
}
&nbsp;
getCurrentTransaction(): Transaction | null {
return this.currentTransaction;
}
&nbsp;
<span class="fstat-no" title="function not covered" > setCurrentTransaction(transaction: Transaction | null): void {</span>
<span class="cstat-no" title="statement not covered" > this.currentTransaction = transaction;</span>
<span class="cstat-no" title="statement not covered" > if (!transaction) {</span>
<span class="cstat-no" title="statement not covered" > this.spanStack = [];</span>
<span class="cstat-no" title="statement not covered" > } else {</span>
<span class="cstat-no" title="statement not covered" > this.spanStack = [transaction];</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
getCurrentSpan(): Span | null {
return this.spanStack.length &gt; 0 ? this.spanStack[this.spanStack.length - 1] : null;
}
&nbsp;
setCurrentSpan(span: Span | null): void {
if (span === null) {
if (this.spanStack.length &gt; 1) {
this.spanStack.pop();
}
} else {
this.spanStack.push(span);
}
}
&nbsp;
startSpan(context: SpanContext): Span | null {
const parentSpan = this.getCurrentSpan();
&nbsp;
if (!parentSpan &amp;&amp; !context.traceId) {
logDebug('TracingManager', 'startSpan skipped: no active span and no traceId');
return null;
}
&nbsp;
let span: Span;
&nbsp;
if (parentSpan) {
span = (parentSpan as SpanImpl).startChild(context);
} else {
span = new SpanImpl({
...context,
parentSpanId: context.parentSpanId,
}) as unknown as Span;
&nbsp;
if (this.currentTransaction) <span class="branch-0 cbranch-no" title="branch not covered" >{</span>
<span class="cstat-no" title="statement not covered" > if (this.currentTransaction.sampled !== false) {</span>
<span class="cstat-no" title="statement not covered" > (this.currentTransaction as unknown as TransactionImpl)._addSpan(span as unknown as SpanImpl);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > (span as SpanImpl &amp; { transaction: Transaction }).transaction = this.currentTransaction;</span>
<span class="cstat-no" title="statement not covered" > }</span>
}
&nbsp;
this.spanStack.push(span);
&nbsp;
logDebug('TracingManager', 'Span started', {
op: span.op,
description: span.description,
spanId: span.spanId,
parentSpanId: span.parentSpanId,
stackDepth: this.spanStack.length,
});
&nbsp;
return span;
}
&nbsp;
onSpanFinished(span: Span): void {
const idx = this.spanStack.lastIndexOf(span);
if (idx !== -1) {
this.spanStack.splice(idx, 1);
}
&nbsp;
if (this.spanStack.length === <span class="branch-0 cbranch-no" title="branch not covered" >0 &amp;&amp; this.currentTransaction) <span class="branch-0 cbranch-no" title="branch not covered" >{</span></span>
<span class="cstat-no" title="statement not covered" > this.spanStack = [this.currentTransaction];</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
logDebug('TracingManager', 'Span finished', {
op: span.op,
description: span.description,
spanId: span.spanId,
stackDepth: this.spanStack.length,
});
}
&nbsp;
onTransactionFinished(transaction: Transaction): void {
if (this.currentTransaction === transaction) {
this.currentTransaction = null;
this.spanStack = [];
}
&nbsp;
logDebug('TracingManager', 'Transaction finished', {
name: transaction.name,
traceId: transaction.traceId,
spanCount: transaction.spans.length,
sampled: transaction.sampled,
});
}
&nbsp;
<span class="fstat-no" title="function not covered" > finishSpan(span: Span): void {</span>
<span class="cstat-no" title="statement not covered" > span.finish();</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
<span class="fstat-no" title="function not covered" > finishTransaction(transaction: Transaction): void {</span>
<span class="cstat-no" title="statement not covered" > transaction.finish();</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
destroy(): void {
if (this.currentTransaction) {
try {
this.currentTransaction.setStatus('aborted');
this.currentTransaction.finish();
<span class="branch-0 cbranch-no" title="branch not covered" > } catch {</span>
<span class="cstat-no" title="statement not covered" > // ignore</span>
<span class="cstat-no" title="statement not covered" > }</span>
}
this.currentTransaction = null;
this.spanStack = [];
}
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-06-25T14:49:54.914Z
</div>
<script src="../../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../../sorter.js"></script>
<script src="../../block-navigation.js"></script>
</body>
</html>

View File

@ -0,0 +1,206 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for src/core</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../../index.html">All files</a> src/core</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">60.64% </span>
<span class="quiet">Statements</span>
<span class='fraction'>974/1606</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">85.78% </span>
<span class="quiet">Branches</span>
<span class='fraction'>169/197</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">52% </span>
<span class="quiet">Functions</span>
<span class='fraction'>52/100</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">60.64% </span>
<span class="quiet">Lines</span>
<span class='fraction'>974/1606</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line medium'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody><tr>
<td class="file low" data-value="Client.ts"><a href="Client.ts.html">Client.ts</a></td>
<td data-value="24.72" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 24%"></div><div class="cover-empty" style="width: 76%"></div></div>
</td>
<td data-value="24.72" class="pct low">24.72%</td>
<td data-value="364" class="abs low">90/364</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="0" class="abs high">0/0</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="29" class="abs low">0/29</td>
<td data-value="24.72" class="pct low">24.72%</td>
<td data-value="364" class="abs low">90/364</td>
</tr>
<tr>
<td class="file high" data-value="ConfigManager.ts"><a href="ConfigManager.ts.html">ConfigManager.ts</a></td>
<td data-value="95.9" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 95%"></div><div class="cover-empty" style="width: 5%"></div></div>
</td>
<td data-value="95.9" class="pct high">95.9%</td>
<td data-value="244" class="abs high">234/244</td>
<td data-value="81.81" class="pct high">81.81%</td>
<td data-value="55" class="abs high">45/55</td>
<td data-value="92.85" class="pct high">92.85%</td>
<td data-value="14" class="abs high">13/14</td>
<td data-value="95.9" class="pct high">95.9%</td>
<td data-value="244" class="abs high">234/244</td>
</tr>
<tr>
<td class="file high" data-value="EventBus.ts"><a href="EventBus.ts.html">EventBus.ts</a></td>
<td data-value="100" class="pic high">
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="47" class="abs high">47/47</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="13" class="abs high">13/13</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="7" class="abs high">7/7</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="47" class="abs high">47/47</td>
</tr>
<tr>
<td class="file high" data-value="EventQueue.ts"><a href="EventQueue.ts.html">EventQueue.ts</a></td>
<td data-value="89.9" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 89%"></div><div class="cover-empty" style="width: 11%"></div></div>
</td>
<td data-value="89.9" class="pct high">89.9%</td>
<td data-value="317" class="abs high">285/317</td>
<td data-value="83.82" class="pct high">83.82%</td>
<td data-value="68" class="abs high">57/68</td>
<td data-value="81.25" class="pct high">81.25%</td>
<td data-value="16" class="abs high">13/16</td>
<td data-value="89.9" class="pct high">89.9%</td>
<td data-value="317" class="abs high">285/317</td>
</tr>
<tr>
<td class="file high" data-value="PluginManager.ts"><a href="PluginManager.ts.html">PluginManager.ts</a></td>
<td data-value="100" class="pic high">
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="87" class="abs high">87/87</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="26" class="abs high">26/26</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="8" class="abs high">8/8</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="87" class="abs high">87/87</td>
</tr>
<tr>
<td class="file low" data-value="Reporter.ts"><a href="Reporter.ts.html">Reporter.ts</a></td>
<td data-value="16.9" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 16%"></div><div class="cover-empty" style="width: 84%"></div></div>
</td>
<td data-value="16.9" class="pct low">16.9%</td>
<td data-value="349" class="abs low">59/349</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="0" class="abs high">0/0</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="12" class="abs low">0/12</td>
<td data-value="16.9" class="pct low">16.9%</td>
<td data-value="349" class="abs low">59/349</td>
</tr>
<tr>
<td class="file high" data-value="TracingManager.ts"><a href="TracingManager.ts.html">TracingManager.ts</a></td>
<td data-value="86.86" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 86%"></div><div class="cover-empty" style="width: 14%"></div></div>
</td>
<td data-value="86.86" class="pct high">86.86%</td>
<td data-value="198" class="abs high">172/198</td>
<td data-value="80" class="pct high">80%</td>
<td data-value="35" class="abs high">28/35</td>
<td data-value="78.57" class="pct medium">78.57%</td>
<td data-value="14" class="abs medium">11/14</td>
<td data-value="86.86" class="pct high">86.86%</td>
<td data-value="198" class="abs high">172/198</td>
</tr>
</tbody>
</table>
</div>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-06-25T14:49:54.914Z
</div>
<script src="../../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../../sorter.js"></script>
<script src="../../block-navigation.js"></script>
</body>
</html>

116
coverage/src/index.html Normal file
View File

@ -0,0 +1,116 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for src</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> src</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">50.39% </span>
<span class="quiet">Statements</span>
<span class='fraction'>127/252</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">0% </span>
<span class="quiet">Functions</span>
<span class='fraction'>0/21</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">50.39% </span>
<span class="quiet">Lines</span>
<span class='fraction'>127/252</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line medium'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody><tr>
<td class="file medium" data-value="index.ts"><a href="index.ts.html">index.ts</a></td>
<td data-value="50.39" class="pic medium">
<div class="chart"><div class="cover-fill" style="width: 50%"></div><div class="cover-empty" style="width: 50%"></div></div>
</td>
<td data-value="50.39" class="pct medium">50.39%</td>
<td data-value="252" class="abs medium">127/252</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="0" class="abs high">0/0</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="21" class="abs low">0/21</td>
<td data-value="50.39" class="pct medium">50.39%</td>
<td data-value="252" class="abs medium">127/252</td>
</tr>
</tbody>
</table>
</div>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-06-25T14:49:54.914Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

841
coverage/src/index.ts.html Normal file
View File

@ -0,0 +1,841 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for src/index.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../prettify.css" />
<link rel="stylesheet" href="../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../index.html">All files</a> / <a href="index.html">src</a> index.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">50.39% </span>
<span class="quiet">Statements</span>
<span class='fraction'>127/252</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">0% </span>
<span class="quiet">Functions</span>
<span class='fraction'>0/21</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">50.39% </span>
<span class="quiet">Lines</span>
<span class='fraction'>127/252</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line medium'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a>
<a name='L103'></a><a href='#L103'>103</a>
<a name='L104'></a><a href='#L104'>104</a>
<a name='L105'></a><a href='#L105'>105</a>
<a name='L106'></a><a href='#L106'>106</a>
<a name='L107'></a><a href='#L107'>107</a>
<a name='L108'></a><a href='#L108'>108</a>
<a name='L109'></a><a href='#L109'>109</a>
<a name='L110'></a><a href='#L110'>110</a>
<a name='L111'></a><a href='#L111'>111</a>
<a name='L112'></a><a href='#L112'>112</a>
<a name='L113'></a><a href='#L113'>113</a>
<a name='L114'></a><a href='#L114'>114</a>
<a name='L115'></a><a href='#L115'>115</a>
<a name='L116'></a><a href='#L116'>116</a>
<a name='L117'></a><a href='#L117'>117</a>
<a name='L118'></a><a href='#L118'>118</a>
<a name='L119'></a><a href='#L119'>119</a>
<a name='L120'></a><a href='#L120'>120</a>
<a name='L121'></a><a href='#L121'>121</a>
<a name='L122'></a><a href='#L122'>122</a>
<a name='L123'></a><a href='#L123'>123</a>
<a name='L124'></a><a href='#L124'>124</a>
<a name='L125'></a><a href='#L125'>125</a>
<a name='L126'></a><a href='#L126'>126</a>
<a name='L127'></a><a href='#L127'>127</a>
<a name='L128'></a><a href='#L128'>128</a>
<a name='L129'></a><a href='#L129'>129</a>
<a name='L130'></a><a href='#L130'>130</a>
<a name='L131'></a><a href='#L131'>131</a>
<a name='L132'></a><a href='#L132'>132</a>
<a name='L133'></a><a href='#L133'>133</a>
<a name='L134'></a><a href='#L134'>134</a>
<a name='L135'></a><a href='#L135'>135</a>
<a name='L136'></a><a href='#L136'>136</a>
<a name='L137'></a><a href='#L137'>137</a>
<a name='L138'></a><a href='#L138'>138</a>
<a name='L139'></a><a href='#L139'>139</a>
<a name='L140'></a><a href='#L140'>140</a>
<a name='L141'></a><a href='#L141'>141</a>
<a name='L142'></a><a href='#L142'>142</a>
<a name='L143'></a><a href='#L143'>143</a>
<a name='L144'></a><a href='#L144'>144</a>
<a name='L145'></a><a href='#L145'>145</a>
<a name='L146'></a><a href='#L146'>146</a>
<a name='L147'></a><a href='#L147'>147</a>
<a name='L148'></a><a href='#L148'>148</a>
<a name='L149'></a><a href='#L149'>149</a>
<a name='L150'></a><a href='#L150'>150</a>
<a name='L151'></a><a href='#L151'>151</a>
<a name='L152'></a><a href='#L152'>152</a>
<a name='L153'></a><a href='#L153'>153</a>
<a name='L154'></a><a href='#L154'>154</a>
<a name='L155'></a><a href='#L155'>155</a>
<a name='L156'></a><a href='#L156'>156</a>
<a name='L157'></a><a href='#L157'>157</a>
<a name='L158'></a><a href='#L158'>158</a>
<a name='L159'></a><a href='#L159'>159</a>
<a name='L160'></a><a href='#L160'>160</a>
<a name='L161'></a><a href='#L161'>161</a>
<a name='L162'></a><a href='#L162'>162</a>
<a name='L163'></a><a href='#L163'>163</a>
<a name='L164'></a><a href='#L164'>164</a>
<a name='L165'></a><a href='#L165'>165</a>
<a name='L166'></a><a href='#L166'>166</a>
<a name='L167'></a><a href='#L167'>167</a>
<a name='L168'></a><a href='#L168'>168</a>
<a name='L169'></a><a href='#L169'>169</a>
<a name='L170'></a><a href='#L170'>170</a>
<a name='L171'></a><a href='#L171'>171</a>
<a name='L172'></a><a href='#L172'>172</a>
<a name='L173'></a><a href='#L173'>173</a>
<a name='L174'></a><a href='#L174'>174</a>
<a name='L175'></a><a href='#L175'>175</a>
<a name='L176'></a><a href='#L176'>176</a>
<a name='L177'></a><a href='#L177'>177</a>
<a name='L178'></a><a href='#L178'>178</a>
<a name='L179'></a><a href='#L179'>179</a>
<a name='L180'></a><a href='#L180'>180</a>
<a name='L181'></a><a href='#L181'>181</a>
<a name='L182'></a><a href='#L182'>182</a>
<a name='L183'></a><a href='#L183'>183</a>
<a name='L184'></a><a href='#L184'>184</a>
<a name='L185'></a><a href='#L185'>185</a>
<a name='L186'></a><a href='#L186'>186</a>
<a name='L187'></a><a href='#L187'>187</a>
<a name='L188'></a><a href='#L188'>188</a>
<a name='L189'></a><a href='#L189'>189</a>
<a name='L190'></a><a href='#L190'>190</a>
<a name='L191'></a><a href='#L191'>191</a>
<a name='L192'></a><a href='#L192'>192</a>
<a name='L193'></a><a href='#L193'>193</a>
<a name='L194'></a><a href='#L194'>194</a>
<a name='L195'></a><a href='#L195'>195</a>
<a name='L196'></a><a href='#L196'>196</a>
<a name='L197'></a><a href='#L197'>197</a>
<a name='L198'></a><a href='#L198'>198</a>
<a name='L199'></a><a href='#L199'>199</a>
<a name='L200'></a><a href='#L200'>200</a>
<a name='L201'></a><a href='#L201'>201</a>
<a name='L202'></a><a href='#L202'>202</a>
<a name='L203'></a><a href='#L203'>203</a>
<a name='L204'></a><a href='#L204'>204</a>
<a name='L205'></a><a href='#L205'>205</a>
<a name='L206'></a><a href='#L206'>206</a>
<a name='L207'></a><a href='#L207'>207</a>
<a name='L208'></a><a href='#L208'>208</a>
<a name='L209'></a><a href='#L209'>209</a>
<a name='L210'></a><a href='#L210'>210</a>
<a name='L211'></a><a href='#L211'>211</a>
<a name='L212'></a><a href='#L212'>212</a>
<a name='L213'></a><a href='#L213'>213</a>
<a name='L214'></a><a href='#L214'>214</a>
<a name='L215'></a><a href='#L215'>215</a>
<a name='L216'></a><a href='#L216'>216</a>
<a name='L217'></a><a href='#L217'>217</a>
<a name='L218'></a><a href='#L218'>218</a>
<a name='L219'></a><a href='#L219'>219</a>
<a name='L220'></a><a href='#L220'>220</a>
<a name='L221'></a><a href='#L221'>221</a>
<a name='L222'></a><a href='#L222'>222</a>
<a name='L223'></a><a href='#L223'>223</a>
<a name='L224'></a><a href='#L224'>224</a>
<a name='L225'></a><a href='#L225'>225</a>
<a name='L226'></a><a href='#L226'>226</a>
<a name='L227'></a><a href='#L227'>227</a>
<a name='L228'></a><a href='#L228'>228</a>
<a name='L229'></a><a href='#L229'>229</a>
<a name='L230'></a><a href='#L230'>230</a>
<a name='L231'></a><a href='#L231'>231</a>
<a name='L232'></a><a href='#L232'>232</a>
<a name='L233'></a><a href='#L233'>233</a>
<a name='L234'></a><a href='#L234'>234</a>
<a name='L235'></a><a href='#L235'>235</a>
<a name='L236'></a><a href='#L236'>236</a>
<a name='L237'></a><a href='#L237'>237</a>
<a name='L238'></a><a href='#L238'>238</a>
<a name='L239'></a><a href='#L239'>239</a>
<a name='L240'></a><a href='#L240'>240</a>
<a name='L241'></a><a href='#L241'>241</a>
<a name='L242'></a><a href='#L242'>242</a>
<a name='L243'></a><a href='#L243'>243</a>
<a name='L244'></a><a href='#L244'>244</a>
<a name='L245'></a><a href='#L245'>245</a>
<a name='L246'></a><a href='#L246'>246</a>
<a name='L247'></a><a href='#L247'>247</a>
<a name='L248'></a><a href='#L248'>248</a>
<a name='L249'></a><a href='#L249'>249</a>
<a name='L250'></a><a href='#L250'>250</a>
<a name='L251'></a><a href='#L251'>251</a>
<a name='L252'></a><a href='#L252'>252</a>
<a name='L253'></a><a href='#L253'>253</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import Client from './core/Client';
import ErrorPlugin from './plugins/ErrorPlugin';
import PerformancePlugin from './plugins/PerformancePlugin';
import NetworkPlugin from './plugins/NetworkPlugin';
import BehaviorPlugin from './plugins/BehaviorPlugin';
import OfflinePlugin from './plugins/OfflinePlugin';
import SamplingPlugin from './plugins/SamplingPlugin';
import RouterPlugin from './plugins/RouterPlugin';
import InteractionPlugin from './plugins/InteractionPlugin';
import { Span as SpanClass, Transaction as TransactionClass, httpStatusToSpanStatus, dbStatusToSpanStatus, statusToSpanStatus } from './utils/tracing';
import { serializeBaggage, parseBaggage, createBaggageFromTransaction } from './utils/baggage';
import type { LightConfig, LightClient, SentryEvent, EventLevel, UserInfo, Breadcrumb, LightPlugin, ErrorEvent, PerformanceEvent, NetworkEvent, BehaviorEvent, DSNInfo, TransactionContext, SpanContext, Transaction, Span, SamplingContext, BaggageFields, SpanStatus } from './types';
import { logDebug, setDebug } from './utils/logger';
import { setAnonymousId as setStoredAnonymousId, clearAnonymousId as clearStoredAnonymousId, getAnonymousId as getStoredAnonymousId } from './utils/identity';
&nbsp;
let globalClient: LightClient | null = null;
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >function init(config: LightConfig): LightClient {</span></span>
<span class="cstat-no" title="statement not covered" > setDebug(config.debug === true);</span>
<span class="cstat-no" title="statement not covered" > logDebug('Index', 'init called', { dsn: config.dsn, debug: config.debug });</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const client = new Client(config);</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const defaultPlugins: LightPlugin[] = [</span>
<span class="cstat-no" title="statement not covered" > new OfflinePlugin(),</span>
<span class="cstat-no" title="statement not covered" > new SamplingPlugin(),</span>
<span class="cstat-no" title="statement not covered" > new ErrorPlugin(),</span>
<span class="cstat-no" title="statement not covered" > ];</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const pluginNames = config.plugins?.filter(p =&gt; typeof p === 'string') as string[] || [];</span>
<span class="cstat-no" title="statement not covered" > </span>
<span class="cstat-no" title="statement not covered" > if (pluginNames.includes('performance') || pluginNames.includes('all')) {</span>
<span class="cstat-no" title="statement not covered" > defaultPlugins.push(new PerformancePlugin());</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > if (pluginNames.includes('network') || pluginNames.includes('all')) {</span>
<span class="cstat-no" title="statement not covered" > defaultPlugins.push(new NetworkPlugin());</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > if (pluginNames.includes('behavior') || pluginNames.includes('all')) {</span>
<span class="cstat-no" title="statement not covered" > defaultPlugins.push(new BehaviorPlugin());</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > if (pluginNames.includes('router') || pluginNames.includes('all')) {</span>
<span class="cstat-no" title="statement not covered" > defaultPlugins.push(new RouterPlugin());</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > if (pluginNames.includes('interaction') || pluginNames.includes('all')) {</span>
<span class="cstat-no" title="statement not covered" > defaultPlugins.push(new InteractionPlugin());</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > logDebug('Index', 'Registering plugins', { count: defaultPlugins.length, plugins: defaultPlugins.map(p =&gt; p.name) });</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > for (const plugin of defaultPlugins) {</span>
<span class="cstat-no" title="statement not covered" > client.use(plugin);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > if (config.plugins) {</span>
<span class="cstat-no" title="statement not covered" > for (const plugin of config.plugins) {</span>
<span class="cstat-no" title="statement not covered" > if (typeof plugin === 'object' &amp;&amp; plugin !== null &amp;&amp; 'name' in plugin &amp;&amp; 'setup' in plugin) {</span>
<span class="cstat-no" title="statement not covered" > client.use(plugin as LightPlugin);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > client.init();</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > globalClient = client;</span>
<span class="cstat-no" title="statement not covered" > logDebug('Index', 'init completed, globalClient set');</span>
<span class="cstat-no" title="statement not covered" > return client;</span>
<span class="cstat-no" title="statement not covered" >}</span>
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >function getClient(): LightClient | null {</span></span>
<span class="cstat-no" title="statement not covered" > return globalClient;</span>
<span class="cstat-no" title="statement not covered" >}</span>
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >function captureException(error: Error | unknown): void {</span></span>
<span class="cstat-no" title="statement not covered" > logDebug('Index', 'captureException called', { hasClient: !!globalClient });</span>
<span class="cstat-no" title="statement not covered" > if (!globalClient) {</span>
<span class="cstat-no" title="statement not covered" > console.warn('[LightSDK] SDK not initialized. Call LightSentry.init() first.');</span>
<span class="cstat-no" title="statement not covered" > return;</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > globalClient.captureException(error);</span>
<span class="cstat-no" title="statement not covered" >}</span>
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >function captureMessage(message: string, level?: EventLevel): void {</span></span>
<span class="cstat-no" title="statement not covered" > logDebug('Index', 'captureMessage called', { hasClient: !!globalClient });</span>
<span class="cstat-no" title="statement not covered" > if (!globalClient) {</span>
<span class="cstat-no" title="statement not covered" > console.warn('[LightSDK] SDK not initialized. Call LightSentry.init() first.');</span>
<span class="cstat-no" title="statement not covered" > return;</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > globalClient.captureMessage(message, level);</span>
<span class="cstat-no" title="statement not covered" >}</span>
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >function captureEvent(event: Partial&lt;SentryEvent&gt; &amp; { type: string }): void {</span></span>
<span class="cstat-no" title="statement not covered" > logDebug('Index', 'captureEvent called', { hasClient: !!globalClient });</span>
<span class="cstat-no" title="statement not covered" > if (!globalClient) {</span>
<span class="cstat-no" title="statement not covered" > console.warn('[LightSDK] SDK not initialized. Call LightSentry.init() first.');</span>
<span class="cstat-no" title="statement not covered" > return;</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > globalClient.captureEvent(event);</span>
<span class="cstat-no" title="statement not covered" >}</span>
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >function setUser(user: UserInfo | null): void {</span></span>
<span class="cstat-no" title="statement not covered" > globalClient?.setUser(user);</span>
<span class="cstat-no" title="statement not covered" >}</span>
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >function setAnonymousId(id: string): void {</span></span>
<span class="cstat-no" title="statement not covered" > setStoredAnonymousId(id);</span>
<span class="cstat-no" title="statement not covered" >}</span>
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >function clearAnonymousId(): void {</span></span>
<span class="cstat-no" title="statement not covered" > clearStoredAnonymousId();</span>
<span class="cstat-no" title="statement not covered" >}</span>
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >function getAnonymousId(): string | null {</span></span>
<span class="cstat-no" title="statement not covered" > return getStoredAnonymousId();</span>
<span class="cstat-no" title="statement not covered" >}</span>
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >function startTransaction(context: TransactionContext): Transaction | null {</span></span>
<span class="cstat-no" title="statement not covered" > return globalClient?.startTransaction(context) || null;</span>
<span class="cstat-no" title="statement not covered" >}</span>
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >function getCurrentTransaction(): Transaction | null {</span></span>
<span class="cstat-no" title="statement not covered" > return globalClient?.getCurrentTransaction() || null;</span>
<span class="cstat-no" title="statement not covered" >}</span>
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >function getCurrentSpan(): Span | null {</span></span>
<span class="cstat-no" title="statement not covered" > return globalClient?.getCurrentSpan() || null;</span>
<span class="cstat-no" title="statement not covered" >}</span>
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >function setSpan(span: Span | null): void {</span></span>
<span class="cstat-no" title="statement not covered" > globalClient?.setSpan(span);</span>
<span class="cstat-no" title="statement not covered" >}</span>
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >function startSpan(context: SpanContext): Span | null {</span></span>
<span class="cstat-no" title="statement not covered" > return globalClient?.startSpan(context) || null;</span>
<span class="cstat-no" title="statement not covered" >}</span>
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >function setTag(key: string, value: string): void {</span></span>
<span class="cstat-no" title="statement not covered" > globalClient?.setTag(key, value);</span>
<span class="cstat-no" title="statement not covered" >}</span>
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >function setTags(tags: Record&lt;string, string&gt;): void {</span></span>
<span class="cstat-no" title="statement not covered" > globalClient?.setTags(tags);</span>
<span class="cstat-no" title="statement not covered" >}</span>
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >function setExtra(key: string, value: unknown): void {</span></span>
<span class="cstat-no" title="statement not covered" > globalClient?.setExtra(key, value);</span>
<span class="cstat-no" title="statement not covered" >}</span>
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >function addBreadcrumb(breadcrumb: Omit&lt;Breadcrumb, 'timestamp'&gt;): void {</span></span>
<span class="cstat-no" title="statement not covered" > globalClient?.addBreadcrumb(breadcrumb);</span>
<span class="cstat-no" title="statement not covered" >}</span>
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >async function flush(): Promise&lt;void&gt; {</span></span>
<span class="cstat-no" title="statement not covered" > await globalClient?.flush();</span>
<span class="cstat-no" title="statement not covered" >}</span>
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >function disable(): void {</span></span>
<span class="cstat-no" title="statement not covered" > globalClient?.disable();</span>
<span class="cstat-no" title="statement not covered" >}</span>
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >function enable(): void {</span></span>
<span class="cstat-no" title="statement not covered" > globalClient?.enable();</span>
<span class="cstat-no" title="statement not covered" >}</span>
&nbsp;
export {
init,
getClient,
captureException,
captureMessage,
captureEvent,
setUser,
setAnonymousId,
clearAnonymousId,
getAnonymousId,
startTransaction,
getCurrentTransaction,
getCurrentSpan,
setSpan,
startSpan,
setTag,
setTags,
setExtra,
addBreadcrumb,
flush,
disable,
enable,
Client,
httpStatusToSpanStatus,
dbStatusToSpanStatus,
statusToSpanStatus,
serializeBaggage,
parseBaggage,
createBaggageFromTransaction,
ErrorPlugin,
PerformancePlugin,
NetworkPlugin,
BehaviorPlugin,
OfflinePlugin,
SamplingPlugin,
RouterPlugin,
InteractionPlugin,
};
&nbsp;
export { SpanClass as Span, TransactionClass as Transaction };
&nbsp;
export type {
LightConfig,
LightClient,
LightPlugin,
SentryEvent,
ErrorEvent,
PerformanceEvent,
NetworkEvent,
BehaviorEvent,
EventLevel,
UserInfo,
Breadcrumb,
DSNInfo,
TransactionContext,
SpanContext,
SamplingContext,
BaggageFields,
SpanStatus,
SpanJSON,
TransactionJSON,
};
&nbsp;
export default {
init,
getClient,
captureException,
captureMessage,
captureEvent,
setUser,
setAnonymousId,
clearAnonymousId,
getAnonymousId,
startTransaction,
getCurrentTransaction,
getCurrentSpan,
setSpan,
startSpan,
setTag,
setTags,
setExtra,
addBreadcrumb,
flush,
disable,
enable,
Span: SpanClass,
Transaction: TransactionClass,
httpStatusToSpanStatus,
};
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-06-25T14:49:54.914Z
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../sorter.js"></script>
<script src="../block-navigation.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,982 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for src/plugins/ErrorPlugin.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../../index.html">All files</a> / <a href="index.html">src/plugins</a> ErrorPlugin.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">17.39% </span>
<span class="quiet">Statements</span>
<span class='fraction'>52/299</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">0% </span>
<span class="quiet">Functions</span>
<span class='fraction'>0/14</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">17.39% </span>
<span class="quiet">Lines</span>
<span class='fraction'>52/299</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line low'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a>
<a name='L103'></a><a href='#L103'>103</a>
<a name='L104'></a><a href='#L104'>104</a>
<a name='L105'></a><a href='#L105'>105</a>
<a name='L106'></a><a href='#L106'>106</a>
<a name='L107'></a><a href='#L107'>107</a>
<a name='L108'></a><a href='#L108'>108</a>
<a name='L109'></a><a href='#L109'>109</a>
<a name='L110'></a><a href='#L110'>110</a>
<a name='L111'></a><a href='#L111'>111</a>
<a name='L112'></a><a href='#L112'>112</a>
<a name='L113'></a><a href='#L113'>113</a>
<a name='L114'></a><a href='#L114'>114</a>
<a name='L115'></a><a href='#L115'>115</a>
<a name='L116'></a><a href='#L116'>116</a>
<a name='L117'></a><a href='#L117'>117</a>
<a name='L118'></a><a href='#L118'>118</a>
<a name='L119'></a><a href='#L119'>119</a>
<a name='L120'></a><a href='#L120'>120</a>
<a name='L121'></a><a href='#L121'>121</a>
<a name='L122'></a><a href='#L122'>122</a>
<a name='L123'></a><a href='#L123'>123</a>
<a name='L124'></a><a href='#L124'>124</a>
<a name='L125'></a><a href='#L125'>125</a>
<a name='L126'></a><a href='#L126'>126</a>
<a name='L127'></a><a href='#L127'>127</a>
<a name='L128'></a><a href='#L128'>128</a>
<a name='L129'></a><a href='#L129'>129</a>
<a name='L130'></a><a href='#L130'>130</a>
<a name='L131'></a><a href='#L131'>131</a>
<a name='L132'></a><a href='#L132'>132</a>
<a name='L133'></a><a href='#L133'>133</a>
<a name='L134'></a><a href='#L134'>134</a>
<a name='L135'></a><a href='#L135'>135</a>
<a name='L136'></a><a href='#L136'>136</a>
<a name='L137'></a><a href='#L137'>137</a>
<a name='L138'></a><a href='#L138'>138</a>
<a name='L139'></a><a href='#L139'>139</a>
<a name='L140'></a><a href='#L140'>140</a>
<a name='L141'></a><a href='#L141'>141</a>
<a name='L142'></a><a href='#L142'>142</a>
<a name='L143'></a><a href='#L143'>143</a>
<a name='L144'></a><a href='#L144'>144</a>
<a name='L145'></a><a href='#L145'>145</a>
<a name='L146'></a><a href='#L146'>146</a>
<a name='L147'></a><a href='#L147'>147</a>
<a name='L148'></a><a href='#L148'>148</a>
<a name='L149'></a><a href='#L149'>149</a>
<a name='L150'></a><a href='#L150'>150</a>
<a name='L151'></a><a href='#L151'>151</a>
<a name='L152'></a><a href='#L152'>152</a>
<a name='L153'></a><a href='#L153'>153</a>
<a name='L154'></a><a href='#L154'>154</a>
<a name='L155'></a><a href='#L155'>155</a>
<a name='L156'></a><a href='#L156'>156</a>
<a name='L157'></a><a href='#L157'>157</a>
<a name='L158'></a><a href='#L158'>158</a>
<a name='L159'></a><a href='#L159'>159</a>
<a name='L160'></a><a href='#L160'>160</a>
<a name='L161'></a><a href='#L161'>161</a>
<a name='L162'></a><a href='#L162'>162</a>
<a name='L163'></a><a href='#L163'>163</a>
<a name='L164'></a><a href='#L164'>164</a>
<a name='L165'></a><a href='#L165'>165</a>
<a name='L166'></a><a href='#L166'>166</a>
<a name='L167'></a><a href='#L167'>167</a>
<a name='L168'></a><a href='#L168'>168</a>
<a name='L169'></a><a href='#L169'>169</a>
<a name='L170'></a><a href='#L170'>170</a>
<a name='L171'></a><a href='#L171'>171</a>
<a name='L172'></a><a href='#L172'>172</a>
<a name='L173'></a><a href='#L173'>173</a>
<a name='L174'></a><a href='#L174'>174</a>
<a name='L175'></a><a href='#L175'>175</a>
<a name='L176'></a><a href='#L176'>176</a>
<a name='L177'></a><a href='#L177'>177</a>
<a name='L178'></a><a href='#L178'>178</a>
<a name='L179'></a><a href='#L179'>179</a>
<a name='L180'></a><a href='#L180'>180</a>
<a name='L181'></a><a href='#L181'>181</a>
<a name='L182'></a><a href='#L182'>182</a>
<a name='L183'></a><a href='#L183'>183</a>
<a name='L184'></a><a href='#L184'>184</a>
<a name='L185'></a><a href='#L185'>185</a>
<a name='L186'></a><a href='#L186'>186</a>
<a name='L187'></a><a href='#L187'>187</a>
<a name='L188'></a><a href='#L188'>188</a>
<a name='L189'></a><a href='#L189'>189</a>
<a name='L190'></a><a href='#L190'>190</a>
<a name='L191'></a><a href='#L191'>191</a>
<a name='L192'></a><a href='#L192'>192</a>
<a name='L193'></a><a href='#L193'>193</a>
<a name='L194'></a><a href='#L194'>194</a>
<a name='L195'></a><a href='#L195'>195</a>
<a name='L196'></a><a href='#L196'>196</a>
<a name='L197'></a><a href='#L197'>197</a>
<a name='L198'></a><a href='#L198'>198</a>
<a name='L199'></a><a href='#L199'>199</a>
<a name='L200'></a><a href='#L200'>200</a>
<a name='L201'></a><a href='#L201'>201</a>
<a name='L202'></a><a href='#L202'>202</a>
<a name='L203'></a><a href='#L203'>203</a>
<a name='L204'></a><a href='#L204'>204</a>
<a name='L205'></a><a href='#L205'>205</a>
<a name='L206'></a><a href='#L206'>206</a>
<a name='L207'></a><a href='#L207'>207</a>
<a name='L208'></a><a href='#L208'>208</a>
<a name='L209'></a><a href='#L209'>209</a>
<a name='L210'></a><a href='#L210'>210</a>
<a name='L211'></a><a href='#L211'>211</a>
<a name='L212'></a><a href='#L212'>212</a>
<a name='L213'></a><a href='#L213'>213</a>
<a name='L214'></a><a href='#L214'>214</a>
<a name='L215'></a><a href='#L215'>215</a>
<a name='L216'></a><a href='#L216'>216</a>
<a name='L217'></a><a href='#L217'>217</a>
<a name='L218'></a><a href='#L218'>218</a>
<a name='L219'></a><a href='#L219'>219</a>
<a name='L220'></a><a href='#L220'>220</a>
<a name='L221'></a><a href='#L221'>221</a>
<a name='L222'></a><a href='#L222'>222</a>
<a name='L223'></a><a href='#L223'>223</a>
<a name='L224'></a><a href='#L224'>224</a>
<a name='L225'></a><a href='#L225'>225</a>
<a name='L226'></a><a href='#L226'>226</a>
<a name='L227'></a><a href='#L227'>227</a>
<a name='L228'></a><a href='#L228'>228</a>
<a name='L229'></a><a href='#L229'>229</a>
<a name='L230'></a><a href='#L230'>230</a>
<a name='L231'></a><a href='#L231'>231</a>
<a name='L232'></a><a href='#L232'>232</a>
<a name='L233'></a><a href='#L233'>233</a>
<a name='L234'></a><a href='#L234'>234</a>
<a name='L235'></a><a href='#L235'>235</a>
<a name='L236'></a><a href='#L236'>236</a>
<a name='L237'></a><a href='#L237'>237</a>
<a name='L238'></a><a href='#L238'>238</a>
<a name='L239'></a><a href='#L239'>239</a>
<a name='L240'></a><a href='#L240'>240</a>
<a name='L241'></a><a href='#L241'>241</a>
<a name='L242'></a><a href='#L242'>242</a>
<a name='L243'></a><a href='#L243'>243</a>
<a name='L244'></a><a href='#L244'>244</a>
<a name='L245'></a><a href='#L245'>245</a>
<a name='L246'></a><a href='#L246'>246</a>
<a name='L247'></a><a href='#L247'>247</a>
<a name='L248'></a><a href='#L248'>248</a>
<a name='L249'></a><a href='#L249'>249</a>
<a name='L250'></a><a href='#L250'>250</a>
<a name='L251'></a><a href='#L251'>251</a>
<a name='L252'></a><a href='#L252'>252</a>
<a name='L253'></a><a href='#L253'>253</a>
<a name='L254'></a><a href='#L254'>254</a>
<a name='L255'></a><a href='#L255'>255</a>
<a name='L256'></a><a href='#L256'>256</a>
<a name='L257'></a><a href='#L257'>257</a>
<a name='L258'></a><a href='#L258'>258</a>
<a name='L259'></a><a href='#L259'>259</a>
<a name='L260'></a><a href='#L260'>260</a>
<a name='L261'></a><a href='#L261'>261</a>
<a name='L262'></a><a href='#L262'>262</a>
<a name='L263'></a><a href='#L263'>263</a>
<a name='L264'></a><a href='#L264'>264</a>
<a name='L265'></a><a href='#L265'>265</a>
<a name='L266'></a><a href='#L266'>266</a>
<a name='L267'></a><a href='#L267'>267</a>
<a name='L268'></a><a href='#L268'>268</a>
<a name='L269'></a><a href='#L269'>269</a>
<a name='L270'></a><a href='#L270'>270</a>
<a name='L271'></a><a href='#L271'>271</a>
<a name='L272'></a><a href='#L272'>272</a>
<a name='L273'></a><a href='#L273'>273</a>
<a name='L274'></a><a href='#L274'>274</a>
<a name='L275'></a><a href='#L275'>275</a>
<a name='L276'></a><a href='#L276'>276</a>
<a name='L277'></a><a href='#L277'>277</a>
<a name='L278'></a><a href='#L278'>278</a>
<a name='L279'></a><a href='#L279'>279</a>
<a name='L280'></a><a href='#L280'>280</a>
<a name='L281'></a><a href='#L281'>281</a>
<a name='L282'></a><a href='#L282'>282</a>
<a name='L283'></a><a href='#L283'>283</a>
<a name='L284'></a><a href='#L284'>284</a>
<a name='L285'></a><a href='#L285'>285</a>
<a name='L286'></a><a href='#L286'>286</a>
<a name='L287'></a><a href='#L287'>287</a>
<a name='L288'></a><a href='#L288'>288</a>
<a name='L289'></a><a href='#L289'>289</a>
<a name='L290'></a><a href='#L290'>290</a>
<a name='L291'></a><a href='#L291'>291</a>
<a name='L292'></a><a href='#L292'>292</a>
<a name='L293'></a><a href='#L293'>293</a>
<a name='L294'></a><a href='#L294'>294</a>
<a name='L295'></a><a href='#L295'>295</a>
<a name='L296'></a><a href='#L296'>296</a>
<a name='L297'></a><a href='#L297'>297</a>
<a name='L298'></a><a href='#L298'>298</a>
<a name='L299'></a><a href='#L299'>299</a>
<a name='L300'></a><a href='#L300'>300</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import type { LightPlugin, LightClient, ErrorEvent, StackFrame, LightConfig } from '../types';
import { now, isObject, matchPatterns } from '../utils/helper';
import { computeFingerprint } from '../utils/hash';
import { sanitizeUrl, shouldIgnoreUrl } from '../utils/env';
import { parseStackTrace } from '../utils/stacktrace';
import { logError } from '../utils/logger';
&nbsp;
interface ErrorPluginConfig {
maxStackFrames?: number;
captureNodeModules?: boolean;
captureColumn?: boolean;
relativePathOnly?: boolean;
sampleRate?: number;
resourceSampleRate?: number;
ignoreErrors?: (string | RegExp)[];
ignoreUrls?: (string | RegExp)[];
includePaths?: (string | RegExp)[];
}
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >class ErrorPlugin implements LightPlugin {</span></span>
<span class="cstat-no" title="statement not covered" > name = 'error';</span>
<span class="cstat-no" title="statement not covered" > version = '1.0.0';</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > private client!: LightClient;</span>
<span class="cstat-no" title="statement not covered" > private config: ErrorPluginConfig = {</span>
<span class="cstat-no" title="statement not covered" > maxStackFrames: 5,</span>
<span class="cstat-no" title="statement not covered" > captureNodeModules: false,</span>
<span class="cstat-no" title="statement not covered" > captureColumn: true,</span>
<span class="cstat-no" title="statement not covered" > relativePathOnly: false,</span>
<span class="cstat-no" title="statement not covered" > resourceSampleRate: 0.1,</span>
<span class="cstat-no" title="statement not covered" > };</span>
&nbsp;
private originalOnError?: typeof window.onerror;
private onUnhandledRejection?: (event: PromiseRejectionEvent) =&gt; void;
private onResourceError?: (event: Event) =&gt; void;
&nbsp;
<span class="fstat-no" title="function not covered" > setup(client: LightClient): void {</span>
<span class="cstat-no" title="statement not covered" > this.client = client;</span>
<span class="cstat-no" title="statement not covered" > this.loadConfig();</span>
<span class="cstat-no" title="statement not covered" > this.setupGlobalError();</span>
<span class="cstat-no" title="statement not covered" > this.setupUnhandledRejection();</span>
<span class="cstat-no" title="statement not covered" > this.setupResourceError();</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >loadConfig(): void {</span>
<span class="cstat-no" title="statement not covered" > const clientConfig = this.client.config as LightConfig;</span>
<span class="cstat-no" title="statement not covered" > if (clientConfig &amp;&amp; isObject(clientConfig.error)) {</span>
<span class="cstat-no" title="statement not covered" > this.config = { ...this.config, ...(clientConfig.error as object) };</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > if (clientConfig.ignoreErrors) {</span>
<span class="cstat-no" title="statement not covered" > this.config.ignoreErrors = [...(this.config.ignoreErrors || []), ...clientConfig.ignoreErrors];</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > if (clientConfig.ignoreUrls) {</span>
<span class="cstat-no" title="statement not covered" > this.config.ignoreUrls = [...(this.config.ignoreUrls || []), ...clientConfig.ignoreUrls];</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > if (clientConfig.includePaths) {</span>
<span class="cstat-no" title="statement not covered" > this.config.includePaths = [...(this.config.includePaths || []), ...clientConfig.includePaths];</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >shouldIgnoreError(message: string): boolean {</span>
<span class="cstat-no" title="statement not covered" > if (!message) return false;</span>
<span class="cstat-no" title="statement not covered" > return matchPatterns(message, this.config.ignoreErrors || []);</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >shouldIgnoreScriptUrl(url: string): boolean {</span>
<span class="cstat-no" title="statement not covered" > return shouldIgnoreUrl(url, this.config.ignoreUrls || []);</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >shouldIncludePath(frames: StackFrame[]): boolean {</span>
<span class="cstat-no" title="statement not covered" > const patterns = this.config.includePaths || [];</span>
<span class="cstat-no" title="statement not covered" > if (patterns.length === 0) return true;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > return frames.some(frame =&gt; </span>
<span class="cstat-no" title="statement not covered" > patterns.some(pattern =&gt; {</span>
<span class="cstat-no" title="statement not covered" > if (typeof pattern === 'string') {</span>
<span class="cstat-no" title="statement not covered" > return frame.filename?.includes(pattern);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > return pattern.test(frame.filename || '');</span>
<span class="cstat-no" title="statement not covered" > })</span>
<span class="cstat-no" title="statement not covered" > );</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >shouldSample(): boolean {</span>
<span class="cstat-no" title="statement not covered" > const rate = this.config.sampleRate ?? 1;</span>
<span class="cstat-no" title="statement not covered" > if (rate &gt;= 1) return true;</span>
<span class="cstat-no" title="statement not covered" > if (rate &lt;= 0) return false;</span>
<span class="cstat-no" title="statement not covered" > return Math.random() &lt; rate;</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >shouldSampleResource(): boolean {</span>
<span class="cstat-no" title="statement not covered" > const rate = this.config.resourceSampleRate ?? 0.1;</span>
<span class="cstat-no" title="statement not covered" > if (rate &gt;= 1) return true;</span>
<span class="cstat-no" title="statement not covered" > if (rate &lt;= 0) return false;</span>
<span class="cstat-no" title="statement not covered" > return Math.random() &lt; rate;</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >setupGlobalError(): void {</span>
<span class="cstat-no" title="statement not covered" > if (typeof window === 'undefined') return;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > this.originalOnError = window.onerror;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > window.onerror = (message, url, lineno, colno, error) =&gt; {</span>
<span class="cstat-no" title="statement not covered" > try {</span>
<span class="cstat-no" title="statement not covered" > if (this.originalOnError) {</span>
<span class="cstat-no" title="statement not covered" > this.originalOnError.call(window, message, url, lineno, colno, error);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > if (url &amp;&amp; this.shouldIgnoreScriptUrl(url as string)) {</span>
<span class="cstat-no" title="statement not covered" > return false;</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const errorEvent = this.buildErrorEvent(</span>
<span class="cstat-no" title="statement not covered" > error || (message as string),</span>
<span class="cstat-no" title="statement not covered" > url as string,</span>
<span class="cstat-no" title="statement not covered" > lineno as number,</span>
<span class="cstat-no" title="statement not covered" > colno as number</span>
<span class="cstat-no" title="statement not covered" > );</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > if (errorEvent) {</span>
<span class="cstat-no" title="statement not covered" > this.client.captureEvent(errorEvent);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > } catch (e) {</span>
<span class="cstat-no" title="statement not covered" > logError('ErrorPlugin', 'Global error handler error', e);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > return false;</span>
<span class="cstat-no" title="statement not covered" > };</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >setupUnhandledRejection(): void {</span>
<span class="cstat-no" title="statement not covered" > if (typeof window === 'undefined') return;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > this.onUnhandledRejection = (event) =&gt; {</span>
<span class="cstat-no" title="statement not covered" > try {</span>
<span class="cstat-no" title="statement not covered" > const reason = event.reason;</span>
<span class="cstat-no" title="statement not covered" > const sourceUrl = this.extractUrlFromReason(reason);</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > if (sourceUrl &amp;&amp; this.shouldIgnoreScriptUrl(sourceUrl)) {</span>
<span class="cstat-no" title="statement not covered" > return;</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const errorEvent = this.buildErrorEvent(reason);</span>
<span class="cstat-no" title="statement not covered" > </span>
<span class="cstat-no" title="statement not covered" > if (errorEvent) {</span>
<span class="cstat-no" title="statement not covered" > if (errorEvent.type === 'error') {</span>
<span class="cstat-no" title="statement not covered" > (errorEvent as ErrorEvent).tags = {</span>
<span class="cstat-no" title="statement not covered" > ...((errorEvent as ErrorEvent).tags || {}),</span>
<span class="cstat-no" title="statement not covered" > unhandled: 'true',</span>
<span class="cstat-no" title="statement not covered" > promise: 'true',</span>
<span class="cstat-no" title="statement not covered" > };</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > this.client.captureEvent(errorEvent);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > } catch (e) {</span>
<span class="cstat-no" title="statement not covered" > logError('ErrorPlugin', 'Unhandledrejection handler error', e);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > };</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > window.addEventListener('unhandledrejection', this.onUnhandledRejection);</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >extractUrlFromReason(reason: unknown): string | null {</span>
<span class="cstat-no" title="statement not covered" > if (reason instanceof Error &amp;&amp; reason.stack) {</span>
<span class="cstat-no" title="statement not covered" > const match = reason.stack.match(/at\s+(?:.+?\s+)?\((https?:\/\/[^:]+):\d+:\d+\)/);</span>
<span class="cstat-no" title="statement not covered" > if (match) {</span>
<span class="cstat-no" title="statement not covered" > return match[1];</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > const urlMatch = reason.stack.match(/(https?:\/\/[^\s)]+):\d+:\d+/);</span>
<span class="cstat-no" title="statement not covered" > if (urlMatch) {</span>
<span class="cstat-no" title="statement not covered" > return urlMatch[1];</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > return null;</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >setupResourceError(): void {</span>
<span class="cstat-no" title="statement not covered" > if (typeof window === 'undefined') return;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > this.onResourceError = (event) =&gt; {</span>
<span class="cstat-no" title="statement not covered" > const target = event.target;</span>
<span class="cstat-no" title="statement not covered" > if (!target) return;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const tagName = (target as HTMLElement).tagName?.toLowerCase();</span>
<span class="cstat-no" title="statement not covered" > const src = (target as HTMLImageElement).src || (target as HTMLLinkElement).href;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > if (tagName &amp;&amp; src &amp;&amp; ['img', 'script', 'link', 'audio', 'video'].includes(tagName)) {</span>
<span class="cstat-no" title="statement not covered" > try {</span>
<span class="cstat-no" title="statement not covered" > if (this.shouldIgnoreScriptUrl(src)) return;</span>
<span class="cstat-no" title="statement not covered" > if (!this.shouldSampleResource()) return;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const fingerprint = `resource:${tagName}`;</span>
<span class="cstat-no" title="statement not covered" > const sanitizedSrc = sanitizeUrl(src);</span>
<span class="cstat-no" title="statement not covered" > this.client.captureEvent({</span>
<span class="cstat-no" title="statement not covered" > type: 'error',</span>
<span class="cstat-no" title="statement not covered" > level: 'warning',</span>
<span class="cstat-no" title="statement not covered" > message: `Resource load failed: ${tagName} ${sanitizedSrc}`,</span>
<span class="cstat-no" title="statement not covered" > timestamp: now(),</span>
<span class="cstat-no" title="statement not covered" > fingerprint,</span>
<span class="cstat-no" title="statement not covered" > tags: {</span>
<span class="cstat-no" title="statement not covered" > resource_type: tagName,</span>
<span class="cstat-no" title="statement not covered" > resource_url: sanitizedSrc,</span>
<span class="cstat-no" title="statement not covered" > },</span>
<span class="cstat-no" title="statement not covered" > exception: {</span>
<span class="cstat-no" title="statement not covered" > type: 'ResourceError',</span>
<span class="cstat-no" title="statement not covered" > value: `Failed to load ${tagName} resource`,</span>
<span class="cstat-no" title="statement not covered" > },</span>
<span class="cstat-no" title="statement not covered" > });</span>
<span class="cstat-no" title="statement not covered" > } catch (e) {</span>
<span class="cstat-no" title="statement not covered" > logError('ErrorPlugin', 'Resource error handler error', e);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > };</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > window.addEventListener('error', this.onResourceError, true);</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >buildErrorEvent(error: Error | unknown, url?: string, lineno?: number, colno?: number): Partial&lt;ErrorEvent&gt; &amp; { type: string } | null {</span>
<span class="cstat-no" title="statement not covered" > const timestamp = now();</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > if (error instanceof Error) {</span>
<span class="cstat-no" title="statement not covered" > if (this.shouldIgnoreError(error.message)) return null;</span>
<span class="cstat-no" title="statement not covered" > </span>
<span class="cstat-no" title="statement not covered" > const frames = parseStackTrace(error.stack, {</span>
<span class="cstat-no" title="statement not covered" > captureNodeModules: this.config.captureNodeModules,</span>
<span class="cstat-no" title="statement not covered" > captureColumn: this.config.captureColumn,</span>
<span class="cstat-no" title="statement not covered" > relativePathOnly: this.config.relativePathOnly,</span>
<span class="cstat-no" title="statement not covered" > maxFrames: this.config.maxStackFrames,</span>
<span class="cstat-no" title="statement not covered" > });</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > if (!this.shouldIncludePath(frames)) return null;</span>
<span class="cstat-no" title="statement not covered" > if (!this.shouldSample()) return null;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const fingerprint = computeFingerprint(error.name, error.message, frames);</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > return {</span>
<span class="cstat-no" title="statement not covered" > type: 'error',</span>
<span class="cstat-no" title="statement not covered" > level: 'error',</span>
<span class="cstat-no" title="statement not covered" > message: error.message,</span>
<span class="cstat-no" title="statement not covered" > timestamp,</span>
<span class="cstat-no" title="statement not covered" > exception: {</span>
<span class="cstat-no" title="statement not covered" > type: error.name,</span>
<span class="cstat-no" title="statement not covered" > value: error.message,</span>
<span class="cstat-no" title="statement not covered" > stacktrace: {</span>
<span class="cstat-no" title="statement not covered" > frames,</span>
<span class="cstat-no" title="statement not covered" > },</span>
<span class="cstat-no" title="statement not covered" > },</span>
<span class="cstat-no" title="statement not covered" > fingerprint,</span>
<span class="cstat-no" title="statement not covered" > };</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const message = String(error);</span>
<span class="cstat-no" title="statement not covered" > if (this.shouldIgnoreError(message)) return null;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const frames: StackFrame[] = [];</span>
<span class="cstat-no" title="statement not covered" > if (url) {</span>
<span class="cstat-no" title="statement not covered" > frames.push({</span>
<span class="cstat-no" title="statement not covered" > filename: url,</span>
<span class="cstat-no" title="statement not covered" > lineno,</span>
<span class="cstat-no" title="statement not covered" > colno: this.config.captureColumn ? colno : undefined,</span>
<span class="cstat-no" title="statement not covered" > in_app: true,</span>
<span class="cstat-no" title="statement not covered" > });</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > if (!this.shouldIncludePath(frames)) return null;</span>
<span class="cstat-no" title="statement not covered" > if (!this.shouldSample()) return null;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > return {</span>
<span class="cstat-no" title="statement not covered" > type: 'error',</span>
<span class="cstat-no" title="statement not covered" > level: 'error',</span>
<span class="cstat-no" title="statement not covered" > message,</span>
<span class="cstat-no" title="statement not covered" > timestamp,</span>
<span class="cstat-no" title="statement not covered" > exception: {</span>
<span class="cstat-no" title="statement not covered" > type: 'Error',</span>
<span class="cstat-no" title="statement not covered" > value: message,</span>
<span class="cstat-no" title="statement not covered" > stacktrace: {</span>
<span class="cstat-no" title="statement not covered" > frames,</span>
<span class="cstat-no" title="statement not covered" > },</span>
<span class="cstat-no" title="statement not covered" > },</span>
<span class="cstat-no" title="statement not covered" > };</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
<span class="fstat-no" title="function not covered" > destroy(): void {</span>
<span class="cstat-no" title="statement not covered" > if (typeof window !== 'undefined') {</span>
<span class="cstat-no" title="statement not covered" > if (this.originalOnError !== undefined) {</span>
<span class="cstat-no" title="statement not covered" > window.onerror = this.originalOnError;</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > if (this.onUnhandledRejection) {</span>
<span class="cstat-no" title="statement not covered" > window.removeEventListener('unhandledrejection', this.onUnhandledRejection);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > if (this.onResourceError) {</span>
<span class="cstat-no" title="statement not covered" > window.removeEventListener('error', this.onResourceError, true);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > }</span>
}
&nbsp;
export default ErrorPlugin;
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-06-25T14:49:54.914Z
</div>
<script src="../../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../../sorter.js"></script>
<script src="../../block-navigation.js"></script>
</body>
</html>

View File

@ -0,0 +1,706 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for src/plugins/InteractionPlugin.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../../index.html">All files</a> / <a href="index.html">src/plugins</a> InteractionPlugin.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">16.9% </span>
<span class="quiet">Statements</span>
<span class='fraction'>35/207</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">0% </span>
<span class="quiet">Functions</span>
<span class='fraction'>0/10</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">16.9% </span>
<span class="quiet">Lines</span>
<span class='fraction'>35/207</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line low'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a>
<a name='L103'></a><a href='#L103'>103</a>
<a name='L104'></a><a href='#L104'>104</a>
<a name='L105'></a><a href='#L105'>105</a>
<a name='L106'></a><a href='#L106'>106</a>
<a name='L107'></a><a href='#L107'>107</a>
<a name='L108'></a><a href='#L108'>108</a>
<a name='L109'></a><a href='#L109'>109</a>
<a name='L110'></a><a href='#L110'>110</a>
<a name='L111'></a><a href='#L111'>111</a>
<a name='L112'></a><a href='#L112'>112</a>
<a name='L113'></a><a href='#L113'>113</a>
<a name='L114'></a><a href='#L114'>114</a>
<a name='L115'></a><a href='#L115'>115</a>
<a name='L116'></a><a href='#L116'>116</a>
<a name='L117'></a><a href='#L117'>117</a>
<a name='L118'></a><a href='#L118'>118</a>
<a name='L119'></a><a href='#L119'>119</a>
<a name='L120'></a><a href='#L120'>120</a>
<a name='L121'></a><a href='#L121'>121</a>
<a name='L122'></a><a href='#L122'>122</a>
<a name='L123'></a><a href='#L123'>123</a>
<a name='L124'></a><a href='#L124'>124</a>
<a name='L125'></a><a href='#L125'>125</a>
<a name='L126'></a><a href='#L126'>126</a>
<a name='L127'></a><a href='#L127'>127</a>
<a name='L128'></a><a href='#L128'>128</a>
<a name='L129'></a><a href='#L129'>129</a>
<a name='L130'></a><a href='#L130'>130</a>
<a name='L131'></a><a href='#L131'>131</a>
<a name='L132'></a><a href='#L132'>132</a>
<a name='L133'></a><a href='#L133'>133</a>
<a name='L134'></a><a href='#L134'>134</a>
<a name='L135'></a><a href='#L135'>135</a>
<a name='L136'></a><a href='#L136'>136</a>
<a name='L137'></a><a href='#L137'>137</a>
<a name='L138'></a><a href='#L138'>138</a>
<a name='L139'></a><a href='#L139'>139</a>
<a name='L140'></a><a href='#L140'>140</a>
<a name='L141'></a><a href='#L141'>141</a>
<a name='L142'></a><a href='#L142'>142</a>
<a name='L143'></a><a href='#L143'>143</a>
<a name='L144'></a><a href='#L144'>144</a>
<a name='L145'></a><a href='#L145'>145</a>
<a name='L146'></a><a href='#L146'>146</a>
<a name='L147'></a><a href='#L147'>147</a>
<a name='L148'></a><a href='#L148'>148</a>
<a name='L149'></a><a href='#L149'>149</a>
<a name='L150'></a><a href='#L150'>150</a>
<a name='L151'></a><a href='#L151'>151</a>
<a name='L152'></a><a href='#L152'>152</a>
<a name='L153'></a><a href='#L153'>153</a>
<a name='L154'></a><a href='#L154'>154</a>
<a name='L155'></a><a href='#L155'>155</a>
<a name='L156'></a><a href='#L156'>156</a>
<a name='L157'></a><a href='#L157'>157</a>
<a name='L158'></a><a href='#L158'>158</a>
<a name='L159'></a><a href='#L159'>159</a>
<a name='L160'></a><a href='#L160'>160</a>
<a name='L161'></a><a href='#L161'>161</a>
<a name='L162'></a><a href='#L162'>162</a>
<a name='L163'></a><a href='#L163'>163</a>
<a name='L164'></a><a href='#L164'>164</a>
<a name='L165'></a><a href='#L165'>165</a>
<a name='L166'></a><a href='#L166'>166</a>
<a name='L167'></a><a href='#L167'>167</a>
<a name='L168'></a><a href='#L168'>168</a>
<a name='L169'></a><a href='#L169'>169</a>
<a name='L170'></a><a href='#L170'>170</a>
<a name='L171'></a><a href='#L171'>171</a>
<a name='L172'></a><a href='#L172'>172</a>
<a name='L173'></a><a href='#L173'>173</a>
<a name='L174'></a><a href='#L174'>174</a>
<a name='L175'></a><a href='#L175'>175</a>
<a name='L176'></a><a href='#L176'>176</a>
<a name='L177'></a><a href='#L177'>177</a>
<a name='L178'></a><a href='#L178'>178</a>
<a name='L179'></a><a href='#L179'>179</a>
<a name='L180'></a><a href='#L180'>180</a>
<a name='L181'></a><a href='#L181'>181</a>
<a name='L182'></a><a href='#L182'>182</a>
<a name='L183'></a><a href='#L183'>183</a>
<a name='L184'></a><a href='#L184'>184</a>
<a name='L185'></a><a href='#L185'>185</a>
<a name='L186'></a><a href='#L186'>186</a>
<a name='L187'></a><a href='#L187'>187</a>
<a name='L188'></a><a href='#L188'>188</a>
<a name='L189'></a><a href='#L189'>189</a>
<a name='L190'></a><a href='#L190'>190</a>
<a name='L191'></a><a href='#L191'>191</a>
<a name='L192'></a><a href='#L192'>192</a>
<a name='L193'></a><a href='#L193'>193</a>
<a name='L194'></a><a href='#L194'>194</a>
<a name='L195'></a><a href='#L195'>195</a>
<a name='L196'></a><a href='#L196'>196</a>
<a name='L197'></a><a href='#L197'>197</a>
<a name='L198'></a><a href='#L198'>198</a>
<a name='L199'></a><a href='#L199'>199</a>
<a name='L200'></a><a href='#L200'>200</a>
<a name='L201'></a><a href='#L201'>201</a>
<a name='L202'></a><a href='#L202'>202</a>
<a name='L203'></a><a href='#L203'>203</a>
<a name='L204'></a><a href='#L204'>204</a>
<a name='L205'></a><a href='#L205'>205</a>
<a name='L206'></a><a href='#L206'>206</a>
<a name='L207'></a><a href='#L207'>207</a>
<a name='L208'></a><a href='#L208'>208</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import type { LightPlugin, LightClient, Transaction, Span, SpanStatus, LightConfig } from '../types';
import { now, isObject } from '../utils/helper';
&nbsp;
interface InteractionPluginConfig {
enable?: boolean;
elementAttribute?: string;
maxTransactionDurationMs?: number;
idleTimeoutMs?: number;
captureScroll?: boolean;
trackClick?: boolean;
trackSubmit?: boolean;
trackKeypress?: boolean;
}
&nbsp;
const DEFAULT_TRACKING_TYPES = ['click', 'submit'];
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >class InteractionPlugin implements LightPlugin {</span></span>
<span class="cstat-no" title="statement not covered" > name = 'interaction';</span>
<span class="cstat-no" title="statement not covered" > version = '1.0.0';</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > private client!: LightClient;</span>
<span class="cstat-no" title="statement not covered" > private config: InteractionPluginConfig = {</span>
<span class="cstat-no" title="statement not covered" > enable: true,</span>
<span class="cstat-no" title="statement not covered" > elementAttribute: 'data-sentry-component',</span>
<span class="cstat-no" title="statement not covered" > maxTransactionDurationMs: 30000,</span>
<span class="cstat-no" title="statement not covered" > idleTimeoutMs: 1000,</span>
<span class="cstat-no" title="statement not covered" > trackClick: true,</span>
<span class="cstat-no" title="statement not covered" > trackSubmit: true,</span>
<span class="cstat-no" title="statement not covered" > trackKeypress: false,</span>
<span class="cstat-no" title="statement not covered" > };</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > private currentTransaction: Transaction | null = null;</span>
<span class="cstat-no" title="statement not covered" > private idleTimer: ReturnType&lt;typeof setTimeout&gt; | null = null;</span>
<span class="cstat-no" title="statement not covered" > private maxDurationTimer: ReturnType&lt;typeof setTimeout&gt; | null = null;</span>
<span class="cstat-no" title="statement not covered" > private debounceTimer: ReturnType&lt;typeof setTimeout&gt; | null = null;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > setup(client: LightClient): void {</span>
<span class="cstat-no" title="statement not covered" > this.client = client;</span>
<span class="cstat-no" title="statement not covered" > this.loadConfig();</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > if (typeof window === 'undefined') return;</span>
<span class="cstat-no" title="statement not covered" > if (!this.config.enable) return;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > if (this.config.trackClick) {</span>
<span class="cstat-no" title="statement not covered" > document.addEventListener('click', this.handleClick, true);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > if (this.config.trackSubmit) {</span>
<span class="cstat-no" title="statement not covered" > document.addEventListener('submit', this.handleSubmit, true);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > private <span class="fstat-no" title="function not covered" >loadConfig(): void {</span></span>
<span class="cstat-no" title="statement not covered" > const clientConfig = this.client.config as LightConfig;</span>
<span class="cstat-no" title="statement not covered" > if (clientConfig &amp;&amp; isObject((clientConfig as Record&lt;string, unknown&gt;).interaction)) {</span>
<span class="cstat-no" title="statement not covered" > this.config = { ...this.config, ...((clientConfig as Record&lt;string, unknown&gt;).interaction as object) };</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > private handleClick = (event: Event): void =&gt; {</span>
<span class="cstat-no" title="statement not covered" > if (this.isEventIgnored(event)) return;</span>
<span class="cstat-no" title="statement not covered" > const target = event.target as HTMLElement;</span>
<span class="cstat-no" title="statement not covered" > const elementName = this.getElementName(target, 'click');</span>
<span class="cstat-no" title="statement not covered" > this.scheduleInteractionTransaction(elementName, 'click', target);</span>
<span class="cstat-no" title="statement not covered" > };</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > private handleSubmit = (event: Event): void =&gt; {</span>
<span class="cstat-no" title="statement not covered" > if (this.isEventIgnored(event)) return;</span>
<span class="cstat-no" title="statement not covered" > const target = event.target as HTMLElement;</span>
<span class="cstat-no" title="statement not covered" > const elementName = this.getElementName(target, 'submit');</span>
<span class="cstat-no" title="statement not covered" > this.scheduleInteractionTransaction(elementName, 'submit', target);</span>
<span class="cstat-no" title="statement not covered" > };</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >isEventIgnored(event: Event): boolean {</span>
<span class="cstat-no" title="statement not covered" > const target = event.target as HTMLElement;</span>
<span class="cstat-no" title="statement not covered" > if (!target) return true;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > if (target.hasAttribute &amp;&amp; target.hasAttribute('data-sentry-ignore')) {</span>
<span class="cstat-no" title="statement not covered" > return true;</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > return false;</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >scheduleInteractionTransaction(name: string, op: string, element: HTMLElement): void {</span>
<span class="cstat-no" title="statement not covered" > if (this.debounceTimer) {</span>
<span class="cstat-no" title="statement not covered" > clearTimeout(this.debounceTimer);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > this.debounceTimer = setTimeout(() =&gt; {</span>
<span class="cstat-no" title="statement not covered" > this.startInteractionTransaction(name, op, element);</span>
<span class="cstat-no" title="statement not covered" > }, 0);</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >getElementName(element: HTMLElement | null, eventType: string): string {</span>
<span class="cstat-no" title="statement not covered" > if (!element) return eventType;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const attr = this.config.elementAttribute || 'data-sentry-component';</span>
<span class="cstat-no" title="statement not covered" > const componentName = element.getAttribute(attr);</span>
<span class="cstat-no" title="statement not covered" > if (componentName) return componentName;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const tag = element.tagName?.toLowerCase() || 'element';</span>
<span class="cstat-no" title="statement not covered" > const id = element.id ? `#${element.id}` : '';</span>
<span class="cstat-no" title="statement not covered" > const classes = element.className &amp;&amp; typeof element.className === 'string'</span>
<span class="cstat-no" title="statement not covered" > ? `.${element.className.split(' ').slice(0, 3).join('.')}`</span>
<span class="cstat-no" title="statement not covered" > : '';</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > return `${eventType}@${tag}${id}${classes}`;</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >startInteractionTransaction(name: string, op: string, element: HTMLElement): void {</span>
<span class="cstat-no" title="statement not covered" > if (!this.client) return;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > if (this.currentTransaction) {</span>
<span class="cstat-no" title="statement not covered" > this.finishCurrentTransaction('ok');</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const transaction = this.client.startTransaction({</span>
<span class="cstat-no" title="statement not covered" > name,</span>
<span class="cstat-no" title="statement not covered" > op: `ui.${op}`,</span>
<span class="cstat-no" title="statement not covered" > description: `User ${op} on ${name}`,</span>
<span class="cstat-no" title="statement not covered" > type: 'user-interaction',</span>
<span class="cstat-no" title="statement not covered" > tags: {</span>
<span class="cstat-no" title="statement not covered" > 'interaction.type': op,</span>
<span class="cstat-no" title="statement not covered" > 'interaction.target': name,</span>
<span class="cstat-no" title="statement not covered" > },</span>
<span class="cstat-no" title="statement not covered" > data: {</span>
<span class="cstat-no" title="statement not covered" > tag: element.tagName?.toLowerCase(),</span>
<span class="cstat-no" title="statement not covered" > id: element.id || undefined,</span>
<span class="cstat-no" title="statement not covered" > text: element.textContent?.substring(0, 100) || undefined,</span>
<span class="cstat-no" title="statement not covered" > },</span>
<span class="cstat-no" title="statement not covered" > });</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > if (transaction) {</span>
<span class="cstat-no" title="statement not covered" > this.currentTransaction = transaction;</span>
<span class="cstat-no" title="statement not covered" > this.scheduleIdleFinish();</span>
<span class="cstat-no" title="statement not covered" > this.scheduleMaxDuration();</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >scheduleIdleFinish(): void {</span>
<span class="cstat-no" title="statement not covered" > if (this.idleTimer) {</span>
<span class="cstat-no" title="statement not covered" > clearTimeout(this.idleTimer);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const idleTimeout = this.config.idleTimeoutMs ?? 1000;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > this.idleTimer = setTimeout(() =&gt; {</span>
<span class="cstat-no" title="statement not covered" > this.finishCurrentTransaction('ok');</span>
<span class="cstat-no" title="statement not covered" > }, idleTimeout);</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >scheduleMaxDuration(): void {</span>
<span class="cstat-no" title="statement not covered" > if (this.maxDurationTimer) {</span>
<span class="cstat-no" title="statement not covered" > clearTimeout(this.maxDurationTimer);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const maxDuration = this.config.maxTransactionDurationMs ?? 30000;</span>
<span class="cstat-no" title="statement not covered" > this.maxDurationTimer = setTimeout(() =&gt; {</span>
<span class="cstat-no" title="statement not covered" > this.finishCurrentTransaction('deadline_exceeded');</span>
<span class="cstat-no" title="statement not covered" > }, maxDuration);</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >finishCurrentTransaction(status: SpanStatus = 'ok'): void {</span>
<span class="cstat-no" title="statement not covered" > if (this.idleTimer) {</span>
<span class="cstat-no" title="statement not covered" > clearTimeout(this.idleTimer);</span>
<span class="cstat-no" title="statement not covered" > this.idleTimer = null;</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > if (this.maxDurationTimer) {</span>
<span class="cstat-no" title="statement not covered" > clearTimeout(this.maxDurationTimer);</span>
<span class="cstat-no" title="statement not covered" > this.maxDurationTimer = null;</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > if (this.debounceTimer) {</span>
<span class="cstat-no" title="statement not covered" > clearTimeout(this.debounceTimer);</span>
<span class="cstat-no" title="statement not covered" > this.debounceTimer = null;</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > if (this.currentTransaction) {</span>
<span class="cstat-no" title="statement not covered" > this.currentTransaction.setStatus(status);</span>
<span class="cstat-no" title="statement not covered" > this.currentTransaction.finish();</span>
<span class="cstat-no" title="statement not covered" > this.currentTransaction = null;</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
<span class="fstat-no" title="function not covered" > destroy(): void {</span>
<span class="cstat-no" title="statement not covered" > if (typeof document !== 'undefined') {</span>
<span class="cstat-no" title="statement not covered" > document.removeEventListener('click', this.handleClick, true);</span>
<span class="cstat-no" title="statement not covered" > document.removeEventListener('submit', this.handleSubmit, true);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > if (this.idleTimer) {</span>
<span class="cstat-no" title="statement not covered" > clearTimeout(this.idleTimer);</span>
<span class="cstat-no" title="statement not covered" > this.idleTimer = null;</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > if (this.maxDurationTimer) {</span>
<span class="cstat-no" title="statement not covered" > clearTimeout(this.maxDurationTimer);</span>
<span class="cstat-no" title="statement not covered" > this.maxDurationTimer = null;</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > if (this.debounceTimer) {</span>
<span class="cstat-no" title="statement not covered" > clearTimeout(this.debounceTimer);</span>
<span class="cstat-no" title="statement not covered" > this.debounceTimer = null;</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > this.finishCurrentTransaction('aborted');</span>
<span class="cstat-no" title="statement not covered" > }</span>
}
&nbsp;
export default InteractionPlugin;
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-06-25T14:49:54.914Z
</div>
<script src="../../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../../sorter.js"></script>
<script src="../../block-navigation.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,997 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for src/plugins/OfflinePlugin.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../../index.html">All files</a> / <a href="index.html">src/plugins</a> OfflinePlugin.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">14.47% </span>
<span class="quiet">Statements</span>
<span class='fraction'>44/304</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">0% </span>
<span class="quiet">Functions</span>
<span class='fraction'>0/15</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">14.47% </span>
<span class="quiet">Lines</span>
<span class='fraction'>44/304</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line low'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a>
<a name='L103'></a><a href='#L103'>103</a>
<a name='L104'></a><a href='#L104'>104</a>
<a name='L105'></a><a href='#L105'>105</a>
<a name='L106'></a><a href='#L106'>106</a>
<a name='L107'></a><a href='#L107'>107</a>
<a name='L108'></a><a href='#L108'>108</a>
<a name='L109'></a><a href='#L109'>109</a>
<a name='L110'></a><a href='#L110'>110</a>
<a name='L111'></a><a href='#L111'>111</a>
<a name='L112'></a><a href='#L112'>112</a>
<a name='L113'></a><a href='#L113'>113</a>
<a name='L114'></a><a href='#L114'>114</a>
<a name='L115'></a><a href='#L115'>115</a>
<a name='L116'></a><a href='#L116'>116</a>
<a name='L117'></a><a href='#L117'>117</a>
<a name='L118'></a><a href='#L118'>118</a>
<a name='L119'></a><a href='#L119'>119</a>
<a name='L120'></a><a href='#L120'>120</a>
<a name='L121'></a><a href='#L121'>121</a>
<a name='L122'></a><a href='#L122'>122</a>
<a name='L123'></a><a href='#L123'>123</a>
<a name='L124'></a><a href='#L124'>124</a>
<a name='L125'></a><a href='#L125'>125</a>
<a name='L126'></a><a href='#L126'>126</a>
<a name='L127'></a><a href='#L127'>127</a>
<a name='L128'></a><a href='#L128'>128</a>
<a name='L129'></a><a href='#L129'>129</a>
<a name='L130'></a><a href='#L130'>130</a>
<a name='L131'></a><a href='#L131'>131</a>
<a name='L132'></a><a href='#L132'>132</a>
<a name='L133'></a><a href='#L133'>133</a>
<a name='L134'></a><a href='#L134'>134</a>
<a name='L135'></a><a href='#L135'>135</a>
<a name='L136'></a><a href='#L136'>136</a>
<a name='L137'></a><a href='#L137'>137</a>
<a name='L138'></a><a href='#L138'>138</a>
<a name='L139'></a><a href='#L139'>139</a>
<a name='L140'></a><a href='#L140'>140</a>
<a name='L141'></a><a href='#L141'>141</a>
<a name='L142'></a><a href='#L142'>142</a>
<a name='L143'></a><a href='#L143'>143</a>
<a name='L144'></a><a href='#L144'>144</a>
<a name='L145'></a><a href='#L145'>145</a>
<a name='L146'></a><a href='#L146'>146</a>
<a name='L147'></a><a href='#L147'>147</a>
<a name='L148'></a><a href='#L148'>148</a>
<a name='L149'></a><a href='#L149'>149</a>
<a name='L150'></a><a href='#L150'>150</a>
<a name='L151'></a><a href='#L151'>151</a>
<a name='L152'></a><a href='#L152'>152</a>
<a name='L153'></a><a href='#L153'>153</a>
<a name='L154'></a><a href='#L154'>154</a>
<a name='L155'></a><a href='#L155'>155</a>
<a name='L156'></a><a href='#L156'>156</a>
<a name='L157'></a><a href='#L157'>157</a>
<a name='L158'></a><a href='#L158'>158</a>
<a name='L159'></a><a href='#L159'>159</a>
<a name='L160'></a><a href='#L160'>160</a>
<a name='L161'></a><a href='#L161'>161</a>
<a name='L162'></a><a href='#L162'>162</a>
<a name='L163'></a><a href='#L163'>163</a>
<a name='L164'></a><a href='#L164'>164</a>
<a name='L165'></a><a href='#L165'>165</a>
<a name='L166'></a><a href='#L166'>166</a>
<a name='L167'></a><a href='#L167'>167</a>
<a name='L168'></a><a href='#L168'>168</a>
<a name='L169'></a><a href='#L169'>169</a>
<a name='L170'></a><a href='#L170'>170</a>
<a name='L171'></a><a href='#L171'>171</a>
<a name='L172'></a><a href='#L172'>172</a>
<a name='L173'></a><a href='#L173'>173</a>
<a name='L174'></a><a href='#L174'>174</a>
<a name='L175'></a><a href='#L175'>175</a>
<a name='L176'></a><a href='#L176'>176</a>
<a name='L177'></a><a href='#L177'>177</a>
<a name='L178'></a><a href='#L178'>178</a>
<a name='L179'></a><a href='#L179'>179</a>
<a name='L180'></a><a href='#L180'>180</a>
<a name='L181'></a><a href='#L181'>181</a>
<a name='L182'></a><a href='#L182'>182</a>
<a name='L183'></a><a href='#L183'>183</a>
<a name='L184'></a><a href='#L184'>184</a>
<a name='L185'></a><a href='#L185'>185</a>
<a name='L186'></a><a href='#L186'>186</a>
<a name='L187'></a><a href='#L187'>187</a>
<a name='L188'></a><a href='#L188'>188</a>
<a name='L189'></a><a href='#L189'>189</a>
<a name='L190'></a><a href='#L190'>190</a>
<a name='L191'></a><a href='#L191'>191</a>
<a name='L192'></a><a href='#L192'>192</a>
<a name='L193'></a><a href='#L193'>193</a>
<a name='L194'></a><a href='#L194'>194</a>
<a name='L195'></a><a href='#L195'>195</a>
<a name='L196'></a><a href='#L196'>196</a>
<a name='L197'></a><a href='#L197'>197</a>
<a name='L198'></a><a href='#L198'>198</a>
<a name='L199'></a><a href='#L199'>199</a>
<a name='L200'></a><a href='#L200'>200</a>
<a name='L201'></a><a href='#L201'>201</a>
<a name='L202'></a><a href='#L202'>202</a>
<a name='L203'></a><a href='#L203'>203</a>
<a name='L204'></a><a href='#L204'>204</a>
<a name='L205'></a><a href='#L205'>205</a>
<a name='L206'></a><a href='#L206'>206</a>
<a name='L207'></a><a href='#L207'>207</a>
<a name='L208'></a><a href='#L208'>208</a>
<a name='L209'></a><a href='#L209'>209</a>
<a name='L210'></a><a href='#L210'>210</a>
<a name='L211'></a><a href='#L211'>211</a>
<a name='L212'></a><a href='#L212'>212</a>
<a name='L213'></a><a href='#L213'>213</a>
<a name='L214'></a><a href='#L214'>214</a>
<a name='L215'></a><a href='#L215'>215</a>
<a name='L216'></a><a href='#L216'>216</a>
<a name='L217'></a><a href='#L217'>217</a>
<a name='L218'></a><a href='#L218'>218</a>
<a name='L219'></a><a href='#L219'>219</a>
<a name='L220'></a><a href='#L220'>220</a>
<a name='L221'></a><a href='#L221'>221</a>
<a name='L222'></a><a href='#L222'>222</a>
<a name='L223'></a><a href='#L223'>223</a>
<a name='L224'></a><a href='#L224'>224</a>
<a name='L225'></a><a href='#L225'>225</a>
<a name='L226'></a><a href='#L226'>226</a>
<a name='L227'></a><a href='#L227'>227</a>
<a name='L228'></a><a href='#L228'>228</a>
<a name='L229'></a><a href='#L229'>229</a>
<a name='L230'></a><a href='#L230'>230</a>
<a name='L231'></a><a href='#L231'>231</a>
<a name='L232'></a><a href='#L232'>232</a>
<a name='L233'></a><a href='#L233'>233</a>
<a name='L234'></a><a href='#L234'>234</a>
<a name='L235'></a><a href='#L235'>235</a>
<a name='L236'></a><a href='#L236'>236</a>
<a name='L237'></a><a href='#L237'>237</a>
<a name='L238'></a><a href='#L238'>238</a>
<a name='L239'></a><a href='#L239'>239</a>
<a name='L240'></a><a href='#L240'>240</a>
<a name='L241'></a><a href='#L241'>241</a>
<a name='L242'></a><a href='#L242'>242</a>
<a name='L243'></a><a href='#L243'>243</a>
<a name='L244'></a><a href='#L244'>244</a>
<a name='L245'></a><a href='#L245'>245</a>
<a name='L246'></a><a href='#L246'>246</a>
<a name='L247'></a><a href='#L247'>247</a>
<a name='L248'></a><a href='#L248'>248</a>
<a name='L249'></a><a href='#L249'>249</a>
<a name='L250'></a><a href='#L250'>250</a>
<a name='L251'></a><a href='#L251'>251</a>
<a name='L252'></a><a href='#L252'>252</a>
<a name='L253'></a><a href='#L253'>253</a>
<a name='L254'></a><a href='#L254'>254</a>
<a name='L255'></a><a href='#L255'>255</a>
<a name='L256'></a><a href='#L256'>256</a>
<a name='L257'></a><a href='#L257'>257</a>
<a name='L258'></a><a href='#L258'>258</a>
<a name='L259'></a><a href='#L259'>259</a>
<a name='L260'></a><a href='#L260'>260</a>
<a name='L261'></a><a href='#L261'>261</a>
<a name='L262'></a><a href='#L262'>262</a>
<a name='L263'></a><a href='#L263'>263</a>
<a name='L264'></a><a href='#L264'>264</a>
<a name='L265'></a><a href='#L265'>265</a>
<a name='L266'></a><a href='#L266'>266</a>
<a name='L267'></a><a href='#L267'>267</a>
<a name='L268'></a><a href='#L268'>268</a>
<a name='L269'></a><a href='#L269'>269</a>
<a name='L270'></a><a href='#L270'>270</a>
<a name='L271'></a><a href='#L271'>271</a>
<a name='L272'></a><a href='#L272'>272</a>
<a name='L273'></a><a href='#L273'>273</a>
<a name='L274'></a><a href='#L274'>274</a>
<a name='L275'></a><a href='#L275'>275</a>
<a name='L276'></a><a href='#L276'>276</a>
<a name='L277'></a><a href='#L277'>277</a>
<a name='L278'></a><a href='#L278'>278</a>
<a name='L279'></a><a href='#L279'>279</a>
<a name='L280'></a><a href='#L280'>280</a>
<a name='L281'></a><a href='#L281'>281</a>
<a name='L282'></a><a href='#L282'>282</a>
<a name='L283'></a><a href='#L283'>283</a>
<a name='L284'></a><a href='#L284'>284</a>
<a name='L285'></a><a href='#L285'>285</a>
<a name='L286'></a><a href='#L286'>286</a>
<a name='L287'></a><a href='#L287'>287</a>
<a name='L288'></a><a href='#L288'>288</a>
<a name='L289'></a><a href='#L289'>289</a>
<a name='L290'></a><a href='#L290'>290</a>
<a name='L291'></a><a href='#L291'>291</a>
<a name='L292'></a><a href='#L292'>292</a>
<a name='L293'></a><a href='#L293'>293</a>
<a name='L294'></a><a href='#L294'>294</a>
<a name='L295'></a><a href='#L295'>295</a>
<a name='L296'></a><a href='#L296'>296</a>
<a name='L297'></a><a href='#L297'>297</a>
<a name='L298'></a><a href='#L298'>298</a>
<a name='L299'></a><a href='#L299'>299</a>
<a name='L300'></a><a href='#L300'>300</a>
<a name='L301'></a><a href='#L301'>301</a>
<a name='L302'></a><a href='#L302'>302</a>
<a name='L303'></a><a href='#L303'>303</a>
<a name='L304'></a><a href='#L304'>304</a>
<a name='L305'></a><a href='#L305'>305</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import type { LightPlugin, LightClient, SentryEvent, LightConfig } from '../types';
import { now } from '../utils/helper';
&nbsp;
const DB_NAME = 'light-sentry-offline';
const STORE_NAME = 'events';
const MAX_EVENTS = 1000;
&nbsp;
interface OfflineEvent {
id: string;
event: SentryEvent;
timestamp: number;
}
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >class OfflinePlugin implements LightPlugin {</span></span>
<span class="cstat-no" title="statement not covered" > name = 'offline';</span>
<span class="cstat-no" title="statement not covered" > version = '1.0.0';</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > private client!: LightClient;</span>
<span class="cstat-no" title="statement not covered" > private config: { maxEvents?: number } = { maxEvents: MAX_EVENTS };</span>
<span class="cstat-no" title="statement not covered" > private db: IDBDatabase | null = null;</span>
<span class="cstat-no" title="statement not covered" > private isOnline: boolean = true;</span>
<span class="cstat-no" title="statement not covered" > private isSyncing: boolean = false;</span>
<span class="cstat-no" title="statement not covered" > private onOnline?: () =&gt; void;</span>
<span class="cstat-no" title="statement not covered" > private onOffline?: () =&gt; void;</span>
<span class="cstat-no" title="statement not covered" > private pendingEvents: SentryEvent[] = [];</span>
&nbsp;
<span class="fstat-no" title="function not covered" > setup(client: LightClient): void {</span>
<span class="cstat-no" title="statement not covered" > this.client = client;</span>
<span class="cstat-no" title="statement not covered" > this.loadConfig();</span>
<span class="cstat-no" title="statement not covered" > this.initDatabase();</span>
<span class="cstat-no" title="statement not covered" > this.setupNetworkListeners();</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >loadConfig(): void {</span>
<span class="cstat-no" title="statement not covered" > const clientConfig = this.client.config as LightConfig;</span>
<span class="cstat-no" title="statement not covered" > if (clientConfig &amp;&amp; typeof clientConfig.offline === 'object') {</span>
<span class="cstat-no" title="statement not covered" > this.config = { ...this.config, ...(clientConfig.offline as object) };</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
<span class="fstat-no" title="function not covered" > private async initDatabase(): Promise&lt;void&gt; {</span>
<span class="cstat-no" title="statement not covered" > if (typeof indexedDB === 'undefined') {</span>
<span class="cstat-no" title="statement not covered" > console.warn('[LightSDK] OfflinePlugin: IndexedDB not available');</span>
<span class="cstat-no" title="statement not covered" > return;</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > return new Promise((resolve, reject) =&gt; {</span>
<span class="cstat-no" title="statement not covered" > const request = indexedDB.open(DB_NAME, 1);</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > request.onerror = () =&gt; {</span>
<span class="cstat-no" title="statement not covered" > console.warn('[LightSDK] OfflinePlugin: Failed to open IndexedDB');</span>
<span class="cstat-no" title="statement not covered" > reject(request.error);</span>
<span class="cstat-no" title="statement not covered" > };</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > request.onsuccess = () =&gt; {</span>
<span class="cstat-no" title="statement not covered" > this.db = request.result;</span>
<span class="cstat-no" title="statement not covered" > resolve();</span>
<span class="cstat-no" title="statement not covered" > };</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > request.onupgradeneeded = (event) =&gt; {</span>
<span class="cstat-no" title="statement not covered" > const db = (event.target as IDBOpenDBRequest).result;</span>
<span class="cstat-no" title="statement not covered" > const currentVersion = event.oldVersion;</span>
<span class="cstat-no" title="statement not covered" > </span>
<span class="cstat-no" title="statement not covered" > if (!db.objectStoreNames.contains(STORE_NAME)) {</span>
<span class="cstat-no" title="statement not covered" > const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' });</span>
<span class="cstat-no" title="statement not covered" > store.createIndex('timestamp', 'timestamp', { unique: false });</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > </span>
<span class="cstat-no" title="statement not covered" > // 未来版本迁移可以在这里处理:</span>
<span class="cstat-no" title="statement not covered" > // if (currentVersion &lt; 2) { ... }</span>
<span class="cstat-no" title="statement not covered" > };</span>
<span class="cstat-no" title="statement not covered" > });</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >setupNetworkListeners(): void {</span>
<span class="cstat-no" title="statement not covered" > if (typeof window === 'undefined') return;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > this.isOnline = navigator.onLine;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > this.onOnline = () =&gt; {</span>
<span class="cstat-no" title="statement not covered" > console.info('[LightSDK] OfflinePlugin: Network online');</span>
<span class="cstat-no" title="statement not covered" > this.isOnline = true;</span>
<span class="cstat-no" title="statement not covered" > this.syncPendingEvents();</span>
<span class="cstat-no" title="statement not covered" > };</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > this.onOffline = () =&gt; {</span>
<span class="cstat-no" title="statement not covered" > console.info('[LightSDK] OfflinePlugin: Network offline');</span>
<span class="cstat-no" title="statement not covered" > this.isOnline = false;</span>
<span class="cstat-no" title="statement not covered" > };</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > window.addEventListener('online', this.onOnline);</span>
<span class="cstat-no" title="statement not covered" > window.addEventListener('offline', this.onOffline);</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >generateId(): string {</span>
<span class="cstat-no" title="statement not covered" > return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
<span class="fstat-no" title="function not covered" > private async saveToIndexedDB(event: SentryEvent): Promise&lt;void&gt; {</span>
<span class="cstat-no" title="statement not covered" > if (!this.db) return;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const maxEvents = this.config.maxEvents || MAX_EVENTS;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const count = await this.getEventCount();</span>
<span class="cstat-no" title="statement not covered" > if (count &gt;= maxEvents) {</span>
<span class="cstat-no" title="statement not covered" > await this.deleteOldestEvents(count - maxEvents + 1);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > return new Promise((resolve, reject) =&gt; {</span>
<span class="cstat-no" title="statement not covered" > const transaction = this.db!.transaction([STORE_NAME], 'readwrite');</span>
<span class="cstat-no" title="statement not covered" > const store = transaction.objectStore(STORE_NAME);</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const offlineEvent: OfflineEvent = {</span>
<span class="cstat-no" title="statement not covered" > id: this.generateId(),</span>
<span class="cstat-no" title="statement not covered" > event: {</span>
<span class="cstat-no" title="statement not covered" > ...event,</span>
<span class="cstat-no" title="statement not covered" > offline: true,</span>
<span class="cstat-no" title="statement not covered" > } as unknown as SentryEvent,</span>
<span class="cstat-no" title="statement not covered" > timestamp: now(),</span>
<span class="cstat-no" title="statement not covered" > };</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const addRequest = store.add(offlineEvent);</span>
<span class="cstat-no" title="statement not covered" > addRequest.onsuccess = () =&gt; resolve();</span>
<span class="cstat-no" title="statement not covered" > addRequest.onerror = () =&gt; reject(addRequest.error);</span>
<span class="cstat-no" title="statement not covered" > });</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >getEventCount(): Promise&lt;number&gt; {</span>
<span class="cstat-no" title="statement not covered" > if (!this.db) return Promise.resolve(0);</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > return new Promise((resolve, reject) =&gt; {</span>
<span class="cstat-no" title="statement not covered" > const transaction = this.db!.transaction([STORE_NAME], 'readonly');</span>
<span class="cstat-no" title="statement not covered" > const store = transaction.objectStore(STORE_NAME);</span>
<span class="cstat-no" title="statement not covered" > const countRequest = store.count();</span>
<span class="cstat-no" title="statement not covered" > countRequest.onsuccess = () =&gt; resolve(countRequest.result);</span>
<span class="cstat-no" title="statement not covered" > countRequest.onerror = () =&gt; reject(countRequest.error);</span>
<span class="cstat-no" title="statement not covered" > });</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
<span class="fstat-no" title="function not covered" > private async deleteOldestEvents(count: number): Promise&lt;void&gt; {</span>
<span class="cstat-no" title="statement not covered" > if (!this.db || count &lt;= 0) return;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > return new Promise((resolve, reject) =&gt; {</span>
<span class="cstat-no" title="statement not covered" > const transaction = this.db!.transaction([STORE_NAME], 'readwrite');</span>
<span class="cstat-no" title="statement not covered" > const store = transaction.objectStore(STORE_NAME);</span>
<span class="cstat-no" title="statement not covered" > const index = store.index('timestamp');</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const cursorRequest = index.openCursor();</span>
<span class="cstat-no" title="statement not covered" > let deleted = 0;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > cursorRequest.onsuccess = (event) =&gt; {</span>
<span class="cstat-no" title="statement not covered" > const cursor = (event.target as IDBRequest&lt;IDBCursorWithValue&gt;).result;</span>
<span class="cstat-no" title="statement not covered" > if (cursor &amp;&amp; deleted &lt; count) {</span>
<span class="cstat-no" title="statement not covered" > cursor.delete();</span>
<span class="cstat-no" title="statement not covered" > deleted++;</span>
<span class="cstat-no" title="statement not covered" > cursor.continue();</span>
<span class="cstat-no" title="statement not covered" > } else {</span>
<span class="cstat-no" title="statement not covered" > resolve();</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > };</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > cursorRequest.onerror = () =&gt; reject(cursorRequest.error);</span>
<span class="cstat-no" title="statement not covered" > });</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
<span class="fstat-no" title="function not covered" > private async getAllStoredEvents(): Promise&lt;OfflineEvent[]&gt; {</span>
<span class="cstat-no" title="statement not covered" > if (!this.db) return [];</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > return new Promise((resolve, reject) =&gt; {</span>
<span class="cstat-no" title="statement not covered" > const transaction = this.db!.transaction([STORE_NAME], 'readonly');</span>
<span class="cstat-no" title="statement not covered" > const store = transaction.objectStore(STORE_NAME);</span>
<span class="cstat-no" title="statement not covered" > const index = store.index('timestamp');</span>
<span class="cstat-no" title="statement not covered" > const request = index.getAll();</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > request.onsuccess = () =&gt; {</span>
<span class="cstat-no" title="statement not covered" > const events = request.result || [];</span>
<span class="cstat-no" title="statement not covered" > // 按时间排序</span>
<span class="cstat-no" title="statement not covered" > events.sort((a, b) =&gt; a.timestamp - b.timestamp);</span>
<span class="cstat-no" title="statement not covered" > resolve(events);</span>
<span class="cstat-no" title="statement not covered" > };</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > request.onerror = () =&gt; reject(request.error);</span>
<span class="cstat-no" title="statement not covered" > });</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
<span class="fstat-no" title="function not covered" > private async clearStoredEvents(): Promise&lt;void&gt; {</span>
<span class="cstat-no" title="statement not covered" > if (!this.db) return;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > return new Promise((resolve, reject) =&gt; {</span>
<span class="cstat-no" title="statement not covered" > const transaction = this.db!.transaction([STORE_NAME], 'readwrite');</span>
<span class="cstat-no" title="statement not covered" > const store = transaction.objectStore(STORE_NAME);</span>
<span class="cstat-no" title="statement not covered" > const request = store.clear();</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > request.onsuccess = () =&gt; resolve();</span>
<span class="cstat-no" title="statement not covered" > request.onerror = () =&gt; reject(request.error);</span>
<span class="cstat-no" title="statement not covered" > });</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
<span class="fstat-no" title="function not covered" > async syncPendingEvents(): Promise&lt;void&gt; {</span>
<span class="cstat-no" title="statement not covered" > if (!this.isOnline || this.isSyncing) return;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > this.isSyncing = true;</span>
<span class="cstat-no" title="statement not covered" > console.info('[LightSDK] OfflinePlugin: Syncing pending events');</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > try {</span>
<span class="cstat-no" title="statement not covered" > let syncedCount = 0;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > // 先处理内存中的事件</span>
<span class="cstat-no" title="statement not covered" > while (this.pendingEvents.length &gt; 0) {</span>
<span class="cstat-no" title="statement not covered" > const event = this.pendingEvents.shift() as Partial&lt;SentryEvent&gt; &amp; { type: string };</span>
<span class="cstat-no" title="statement not covered" > try {</span>
<span class="cstat-no" title="statement not covered" > this.client.captureEvent(event);</span>
<span class="cstat-no" title="statement not covered" > syncedCount++;</span>
<span class="cstat-no" title="statement not covered" > } catch (error) {</span>
<span class="cstat-no" title="statement not covered" > console.error('[LightSDK] OfflinePlugin: Failed to re-enqueue memory event', error);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > // 再处理 IndexedDB 中的事件</span>
<span class="cstat-no" title="statement not covered" > const storedEvents = await this.getAllStoredEvents();</span>
<span class="cstat-no" title="statement not covered" > if (storedEvents.length &gt; 0) {</span>
<span class="cstat-no" title="statement not covered" > for (const offlineEvent of storedEvents) {</span>
<span class="cstat-no" title="statement not covered" > try {</span>
<span class="cstat-no" title="statement not covered" > const event = offlineEvent.event as Partial&lt;SentryEvent&gt; &amp; { type: string };</span>
<span class="cstat-no" title="statement not covered" > this.client.captureEvent(event);</span>
<span class="cstat-no" title="statement not covered" > syncedCount++;</span>
<span class="cstat-no" title="statement not covered" > } catch (error) {</span>
<span class="cstat-no" title="statement not covered" > console.error('[LightSDK] OfflinePlugin: Failed to re-enqueue event', error);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > // 等待队列 flush</span>
<span class="cstat-no" title="statement not covered" > try {</span>
<span class="cstat-no" title="statement not covered" > await this.client.flush();</span>
<span class="cstat-no" title="statement not covered" > } catch (e) {</span>
<span class="cstat-no" title="statement not covered" > // flush 失败不影响,事件已在队列中会自动重试</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > // 清空已同步的事件</span>
<span class="cstat-no" title="statement not covered" > await this.clearStoredEvents();</span>
<span class="cstat-no" title="statement not covered" > console.info(`[LightSDK] OfflinePlugin: Synced ${syncedCount} events`);</span>
<span class="cstat-no" title="statement not covered" > } catch (error) {</span>
<span class="cstat-no" title="statement not covered" > console.error('[LightSDK] OfflinePlugin: Sync failed', error);</span>
<span class="cstat-no" title="statement not covered" > } finally {</span>
<span class="cstat-no" title="statement not covered" > this.isSyncing = false;</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
<span class="fstat-no" title="function not covered" > beforeReport(event: SentryEvent): SentryEvent | null {</span>
<span class="cstat-no" title="statement not covered" > if (!this.isOnline) {</span>
<span class="cstat-no" title="statement not covered" > const maxEvents = this.config.maxEvents || MAX_EVENTS;</span>
<span class="cstat-no" title="statement not covered" > if (this.pendingEvents.length &gt;= maxEvents) {</span>
<span class="cstat-no" title="statement not covered" > this.pendingEvents.shift();</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > this.pendingEvents.push(event);</span>
<span class="cstat-no" title="statement not covered" > this.savePendingEventsToDB();</span>
<span class="cstat-no" title="statement not covered" > return null;</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const onlineEvent = { ...event };</span>
<span class="cstat-no" title="statement not covered" > delete (onlineEvent as Record&lt;string, unknown&gt;).offline;</span>
<span class="cstat-no" title="statement not covered" > return onlineEvent as SentryEvent;</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >savePendingEventsToDB(): void {</span>
<span class="cstat-no" title="statement not covered" > if (this.pendingEvents.length === 0) return;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const events = this.pendingEvents.splice(0, this.pendingEvents.length);</span>
<span class="cstat-no" title="statement not covered" > let savedCount = 0;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const saveNext = () =&gt; {</span>
<span class="cstat-no" title="statement not covered" > if (savedCount &gt;= events.length) return;</span>
<span class="cstat-no" title="statement not covered" > const event = events[savedCount];</span>
<span class="cstat-no" title="statement not covered" > this.saveToIndexedDB(event).then(() =&gt; {</span>
<span class="cstat-no" title="statement not covered" > savedCount++;</span>
<span class="cstat-no" title="statement not covered" > saveNext();</span>
<span class="cstat-no" title="statement not covered" > }).catch(err =&gt; {</span>
<span class="cstat-no" title="statement not covered" > console.error('[LightSDK] OfflinePlugin: Failed to save event offline', err);</span>
<span class="cstat-no" title="statement not covered" > // 恢复剩余未保存的事件</span>
<span class="cstat-no" title="statement not covered" > this.pendingEvents.unshift(...events.slice(savedCount));</span>
<span class="cstat-no" title="statement not covered" > });</span>
<span class="cstat-no" title="statement not covered" > };</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > saveNext();</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
<span class="fstat-no" title="function not covered" > destroy(): void {</span>
<span class="cstat-no" title="statement not covered" > if (typeof window !== 'undefined') {</span>
<span class="cstat-no" title="statement not covered" > if (this.onOnline) {</span>
<span class="cstat-no" title="statement not covered" > window.removeEventListener('online', this.onOnline);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > if (this.onOffline) {</span>
<span class="cstat-no" title="statement not covered" > window.removeEventListener('offline', this.onOffline);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > if (this.db) {</span>
<span class="cstat-no" title="statement not covered" > this.db.close();</span>
<span class="cstat-no" title="statement not covered" > this.db = null;</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > }</span>
}
&nbsp;
export default OfflinePlugin;
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-06-25T14:49:54.914Z
</div>
<script src="../../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../../sorter.js"></script>
<script src="../../block-navigation.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,628 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for src/plugins/RouterPlugin.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../../index.html">All files</a> / <a href="index.html">src/plugins</a> RouterPlugin.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">14.36% </span>
<span class="quiet">Statements</span>
<span class='fraction'>26/181</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">0% </span>
<span class="quiet">Functions</span>
<span class='fraction'>0/10</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">14.36% </span>
<span class="quiet">Lines</span>
<span class='fraction'>26/181</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line low'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a>
<a name='L103'></a><a href='#L103'>103</a>
<a name='L104'></a><a href='#L104'>104</a>
<a name='L105'></a><a href='#L105'>105</a>
<a name='L106'></a><a href='#L106'>106</a>
<a name='L107'></a><a href='#L107'>107</a>
<a name='L108'></a><a href='#L108'>108</a>
<a name='L109'></a><a href='#L109'>109</a>
<a name='L110'></a><a href='#L110'>110</a>
<a name='L111'></a><a href='#L111'>111</a>
<a name='L112'></a><a href='#L112'>112</a>
<a name='L113'></a><a href='#L113'>113</a>
<a name='L114'></a><a href='#L114'>114</a>
<a name='L115'></a><a href='#L115'>115</a>
<a name='L116'></a><a href='#L116'>116</a>
<a name='L117'></a><a href='#L117'>117</a>
<a name='L118'></a><a href='#L118'>118</a>
<a name='L119'></a><a href='#L119'>119</a>
<a name='L120'></a><a href='#L120'>120</a>
<a name='L121'></a><a href='#L121'>121</a>
<a name='L122'></a><a href='#L122'>122</a>
<a name='L123'></a><a href='#L123'>123</a>
<a name='L124'></a><a href='#L124'>124</a>
<a name='L125'></a><a href='#L125'>125</a>
<a name='L126'></a><a href='#L126'>126</a>
<a name='L127'></a><a href='#L127'>127</a>
<a name='L128'></a><a href='#L128'>128</a>
<a name='L129'></a><a href='#L129'>129</a>
<a name='L130'></a><a href='#L130'>130</a>
<a name='L131'></a><a href='#L131'>131</a>
<a name='L132'></a><a href='#L132'>132</a>
<a name='L133'></a><a href='#L133'>133</a>
<a name='L134'></a><a href='#L134'>134</a>
<a name='L135'></a><a href='#L135'>135</a>
<a name='L136'></a><a href='#L136'>136</a>
<a name='L137'></a><a href='#L137'>137</a>
<a name='L138'></a><a href='#L138'>138</a>
<a name='L139'></a><a href='#L139'>139</a>
<a name='L140'></a><a href='#L140'>140</a>
<a name='L141'></a><a href='#L141'>141</a>
<a name='L142'></a><a href='#L142'>142</a>
<a name='L143'></a><a href='#L143'>143</a>
<a name='L144'></a><a href='#L144'>144</a>
<a name='L145'></a><a href='#L145'>145</a>
<a name='L146'></a><a href='#L146'>146</a>
<a name='L147'></a><a href='#L147'>147</a>
<a name='L148'></a><a href='#L148'>148</a>
<a name='L149'></a><a href='#L149'>149</a>
<a name='L150'></a><a href='#L150'>150</a>
<a name='L151'></a><a href='#L151'>151</a>
<a name='L152'></a><a href='#L152'>152</a>
<a name='L153'></a><a href='#L153'>153</a>
<a name='L154'></a><a href='#L154'>154</a>
<a name='L155'></a><a href='#L155'>155</a>
<a name='L156'></a><a href='#L156'>156</a>
<a name='L157'></a><a href='#L157'>157</a>
<a name='L158'></a><a href='#L158'>158</a>
<a name='L159'></a><a href='#L159'>159</a>
<a name='L160'></a><a href='#L160'>160</a>
<a name='L161'></a><a href='#L161'>161</a>
<a name='L162'></a><a href='#L162'>162</a>
<a name='L163'></a><a href='#L163'>163</a>
<a name='L164'></a><a href='#L164'>164</a>
<a name='L165'></a><a href='#L165'>165</a>
<a name='L166'></a><a href='#L166'>166</a>
<a name='L167'></a><a href='#L167'>167</a>
<a name='L168'></a><a href='#L168'>168</a>
<a name='L169'></a><a href='#L169'>169</a>
<a name='L170'></a><a href='#L170'>170</a>
<a name='L171'></a><a href='#L171'>171</a>
<a name='L172'></a><a href='#L172'>172</a>
<a name='L173'></a><a href='#L173'>173</a>
<a name='L174'></a><a href='#L174'>174</a>
<a name='L175'></a><a href='#L175'>175</a>
<a name='L176'></a><a href='#L176'>176</a>
<a name='L177'></a><a href='#L177'>177</a>
<a name='L178'></a><a href='#L178'>178</a>
<a name='L179'></a><a href='#L179'>179</a>
<a name='L180'></a><a href='#L180'>180</a>
<a name='L181'></a><a href='#L181'>181</a>
<a name='L182'></a><a href='#L182'>182</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import type { LightPlugin, LightClient, Transaction, SpanStatus, LightConfig } from '../types';
import { now, isObject } from '../utils/helper';
&nbsp;
interface RouterPluginConfig {
enable?: boolean;
instrumentHistory?: boolean;
instrumentHash?: boolean;
routeNameFormatter?: (url: string) =&gt; string;
maxTransactionDurationMs?: number;
}
&nbsp;
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >class RouterPlugin implements LightPlugin {</span></span>
<span class="cstat-no" title="statement not covered" > name = 'router';</span>
<span class="cstat-no" title="statement not covered" > version = '1.0.0';</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > private client!: LightClient;</span>
<span class="cstat-no" title="statement not covered" > private config: RouterPluginConfig = {</span>
<span class="cstat-no" title="statement not covered" > enable: true,</span>
<span class="cstat-no" title="statement not covered" > instrumentHistory: true,</span>
<span class="cstat-no" title="statement not covered" > instrumentHash: true,</span>
<span class="cstat-no" title="statement not covered" > maxTransactionDurationMs: 30000,</span>
<span class="cstat-no" title="statement not covered" > };</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > private currentTransaction: Transaction | null = null;</span>
<span class="cstat-no" title="statement not covered" > private lastRoute: string = '';</span>
<span class="cstat-no" title="statement not covered" > private originalPushState?: typeof history.pushState;</span>
<span class="cstat-no" title="statement not covered" > private originalReplaceState?: typeof history.replaceState;</span>
<span class="cstat-no" title="statement not covered" > private maxDurationTimer: ReturnType&lt;typeof setTimeout&gt; | null = null;</span>
<span class="cstat-no" title="statement not covered" > private routeChangeTimer: ReturnType&lt;typeof setTimeout&gt; | null = null;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > setup(client: LightClient): void {</span>
<span class="cstat-no" title="statement not covered" > this.client = client;</span>
<span class="cstat-no" title="statement not covered" > this.loadConfig();</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > if (typeof window === 'undefined') return;</span>
<span class="cstat-no" title="statement not covered" > if (!this.config.enable) return;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > if (this.config.instrumentHistory) {</span>
<span class="cstat-no" title="statement not covered" > this.instrumentHistory();</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > if (this.config.instrumentHash) {</span>
<span class="cstat-no" title="statement not covered" > this.instrumentHash();</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > private <span class="fstat-no" title="function not covered" >loadConfig(): void {</span></span>
<span class="cstat-no" title="statement not covered" > const clientConfig = this.client.config as LightConfig;</span>
<span class="cstat-no" title="statement not covered" > if (clientConfig &amp;&amp; isObject((clientConfig as Record&lt;string, unknown&gt;).router)) {</span>
<span class="cstat-no" title="statement not covered" > this.config = { ...this.config, ...((clientConfig as Record&lt;string, unknown&gt;).router as object) };</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > private <span class="fstat-no" title="function not covered" >instrumentHistory(): void {</span></span>
<span class="cstat-no" title="statement not covered" > if (typeof history === 'undefined' || !history.pushState) return;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > this.originalPushState = history.pushState.bind(history);</span>
<span class="cstat-no" title="statement not covered" > this.originalReplaceState = history.replaceState.bind(history);</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > history.pushState = (data: unknown, title: string, url?: string | null): void =&gt; {</span>
<span class="cstat-no" title="statement not covered" > this.originalPushState!(data, title, url);</span>
<span class="cstat-no" title="statement not covered" > this.handleRouteChange(url || location.pathname + location.search, 'pushState');</span>
<span class="cstat-no" title="statement not covered" > };</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > history.replaceState = (data: unknown, title: string, url?: string | null): void =&gt; {</span>
<span class="cstat-no" title="statement not covered" > this.originalReplaceState!(data, title, url);</span>
<span class="cstat-no" title="statement not covered" > this.handleRouteChange(url || location.pathname + location.search, 'replaceState');</span>
<span class="cstat-no" title="statement not covered" > };</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > window.addEventListener('popstate', this.handlePopState);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > private handlePopState = (): void =&gt; {</span>
<span class="cstat-no" title="statement not covered" > this.handleRouteChange(location.pathname + location.search, 'popstate');</span>
<span class="cstat-no" title="statement not covered" > };</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > private <span class="fstat-no" title="function not covered" >instrumentHash(): void {</span></span>
<span class="cstat-no" title="statement not covered" > window.addEventListener('hashchange', this.handleHashChange);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > private handleHashChange = (): void =&gt; {</span>
<span class="cstat-no" title="statement not covered" > this.handleRouteChange(location.hash || '#/', 'hashchange');</span>
<span class="cstat-no" title="statement not covered" > };</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >handleRouteChange(to: string, source: string): void {</span>
<span class="cstat-no" title="statement not covered" > if (this.routeChangeTimer) {</span>
<span class="cstat-no" title="statement not covered" > clearTimeout(this.routeChangeTimer);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > this.routeChangeTimer = setTimeout(() =&gt; {</span>
<span class="cstat-no" title="statement not covered" > this.routeChangeTimer = null;</span>
<span class="cstat-no" title="statement not covered" > this.processRouteChange(to, source);</span>
<span class="cstat-no" title="statement not covered" > }, 50);</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >processRouteChange(to: string, source: string): void {</span>
<span class="cstat-no" title="statement not covered" > const routeName = this.config.routeNameFormatter</span>
<span class="cstat-no" title="statement not covered" > ? this.config.routeNameFormatter(to)</span>
<span class="cstat-no" title="statement not covered" > : to.split('?')[0];</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > if (routeName === this.lastRoute) return;</span>
<span class="cstat-no" title="statement not covered" > this.lastRoute = routeName;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > this.finishCurrentTransaction('ok');</span>
<span class="cstat-no" title="statement not covered" > this.startNavigationTransaction(routeName, source);</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >startNavigationTransaction(name: string, source: string): void {</span>
<span class="cstat-no" title="statement not covered" > if (!this.client) return;</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const transaction = this.client.startTransaction({</span>
<span class="cstat-no" title="statement not covered" > name,</span>
<span class="cstat-no" title="statement not covered" > op: 'navigation',</span>
<span class="cstat-no" title="statement not covered" > description: `Navigation to ${name}`,</span>
<span class="cstat-no" title="statement not covered" > type: 'navigation',</span>
<span class="cstat-no" title="statement not covered" > tags: {</span>
<span class="cstat-no" title="statement not covered" > 'route.name': name,</span>
<span class="cstat-no" title="statement not covered" > 'route.source': source,</span>
<span class="cstat-no" title="statement not covered" > },</span>
<span class="cstat-no" title="statement not covered" > });</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > if (transaction) {</span>
<span class="cstat-no" title="statement not covered" > this.currentTransaction = transaction;</span>
<span class="cstat-no" title="statement not covered" > this.startMaxDurationTimer();</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >startMaxDurationTimer(): void {</span>
<span class="cstat-no" title="statement not covered" > if (this.maxDurationTimer) {</span>
<span class="cstat-no" title="statement not covered" > clearTimeout(this.maxDurationTimer);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > const maxDuration = this.config.maxTransactionDurationMs ?? 30000;</span>
<span class="cstat-no" title="statement not covered" > this.maxDurationTimer = setTimeout(() =&gt; {</span>
<span class="cstat-no" title="statement not covered" > if (this.currentTransaction) {</span>
<span class="cstat-no" title="statement not covered" > this.currentTransaction.setStatus('deadline_exceeded');</span>
<span class="cstat-no" title="statement not covered" > this.finishCurrentTransaction('deadline_exceeded');</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > }, maxDuration);</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
private <span class="fstat-no" title="function not covered" >finishCurrentTransaction(status: SpanStatus = 'ok'): void {</span>
<span class="cstat-no" title="statement not covered" > if (this.maxDurationTimer) {</span>
<span class="cstat-no" title="statement not covered" > clearTimeout(this.maxDurationTimer);</span>
<span class="cstat-no" title="statement not covered" > this.maxDurationTimer = null;</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > if (this.currentTransaction) {</span>
<span class="cstat-no" title="statement not covered" > this.currentTransaction.setStatus(status);</span>
<span class="cstat-no" title="statement not covered" > this.currentTransaction.finish();</span>
<span class="cstat-no" title="statement not covered" > this.currentTransaction = null;</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
<span class="fstat-no" title="function not covered" > destroy(): void {</span>
<span class="cstat-no" title="statement not covered" > if (this.originalPushState &amp;&amp; typeof history !== 'undefined') {</span>
<span class="cstat-no" title="statement not covered" > history.pushState = this.originalPushState;</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > if (this.originalReplaceState &amp;&amp; typeof history !== 'undefined') {</span>
<span class="cstat-no" title="statement not covered" > history.replaceState = this.originalReplaceState;</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > if (typeof window !== 'undefined') {</span>
<span class="cstat-no" title="statement not covered" > window.removeEventListener('popstate', this.handlePopState);</span>
<span class="cstat-no" title="statement not covered" > window.removeEventListener('hashchange', this.handleHashChange);</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > if (this.maxDurationTimer) {</span>
<span class="cstat-no" title="statement not covered" > clearTimeout(this.maxDurationTimer);</span>
<span class="cstat-no" title="statement not covered" > this.maxDurationTimer = null;</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" > if (this.routeChangeTimer) {</span>
<span class="cstat-no" title="statement not covered" > clearTimeout(this.routeChangeTimer);</span>
<span class="cstat-no" title="statement not covered" > this.routeChangeTimer = null;</span>
<span class="cstat-no" title="statement not covered" > }</span>
<span class="cstat-no" title="statement not covered" ></span>
<span class="cstat-no" title="statement not covered" > this.finishCurrentTransaction('aborted');</span>
<span class="cstat-no" title="statement not covered" > }</span>
}
&nbsp;
export default RouterPlugin;
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-06-25T14:49:54.914Z
</div>
<script src="../../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../../sorter.js"></script>
<script src="../../block-navigation.js"></script>
</body>
</html>

View File

@ -0,0 +1,367 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for src/plugins/SamplingPlugin.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../../index.html">All files</a> / <a href="index.html">src/plugins</a> SamplingPlugin.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>94/94</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">89.28% </span>
<span class="quiet">Branches</span>
<span class='fraction'>25/28</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>8/8</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>94/94</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import type { LightPlugin, LightClient, SentryEvent, LightConfig } from '../types';
import { isObject } from '../utils/helper';
import { logDebug } from '../utils/logger';
&nbsp;
interface SampleRateConfig {
error?: number;
performance?: number;
network?: number;
behavior?: number;
[key: string]: number | undefined;
}
&nbsp;
interface SamplingPluginConfig {
rates?: SampleRateConfig;
}
&nbsp;
class SamplingPlugin implements LightPlugin {
name = 'sampling';
version = '1.0.0';
&nbsp;
private client!: LightClient;
private config: SamplingPluginConfig = {
rates: {
error: 1,
performance: 1,
network: 1,
behavior: 1,
},
};
&nbsp;
setup(client: LightClient): void {
this.client = client;
this.loadConfig();
}
&nbsp;
private loadConfig(): void {
const clientConfig = this.client.config as LightConfig;
if (clientConfig &amp;&amp; isObject(clientConfig.sampling)) {
const samplingConfig = clientConfig.sampling as object;
if ('rates' in samplingConfig &amp;&amp; isObject((samplingConfig as { rates?: object }).rates)) {
this.config.rates = { ...this.config.rates, ...((samplingConfig as { rates: object }).rates as object) } as SampleRateConfig;
}
}
}
&nbsp;
private shouldSample(event: SentryEvent): boolean {
const globalRate = (this.client.config as LightConfig).<span class="branch-0 cbranch-no" title="branch not covered" >sampleRate ?? 1;</span>
if (globalRate &lt;= 0) return false;
&nbsp;
const typeRate = this.getTypeRate(event);
&nbsp;
if (globalRate &gt;= 1) {
return this.randomCheck(typeRate);
}
return this.randomCheck(globalRate * typeRate);
}
&nbsp;
private getTypeRate(event: SentryEvent): number {
const rates = this.config.<span class="branch-0 cbranch-no" title="branch not covered" >rates || {};</span>
const type = event.type;
&nbsp;
if (type === 'count') {
return 1;
}
&nbsp;
const rate = rates[type];
return rate !== undefined ? rate : 1;
}
&nbsp;
private randomCheck(rate: number): boolean {
if (rate &gt;= 1) return true;
if (rate &lt;= 0) return false;
return Math.random() &lt; rate;
}
&nbsp;
beforeReport(event: SentryEvent): SentryEvent | null {
const sampled = this.shouldSample(event);
if (!sampled) {
logDebug('SamplingPlugin', 'Event dropped by sampling', {
type: event.type,
globalRate: (this.client.config as LightConfig).<span class="branch-0 cbranch-no" title="branch not covered" >sampleRate ?? 1,</span>
typeRate: this.getTypeRate(event),
});
return null;
}
return event;
}
&nbsp;
destroy(): void {
// nothing to clean up
}
}
&nbsp;
export default SamplingPlugin;
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-06-25T14:49:54.914Z
</div>
<script src="../../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../../sorter.js"></script>
<script src="../../block-navigation.js"></script>
</body>
</html>

View File

@ -0,0 +1,221 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for src/plugins</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../../index.html">All files</a> src/plugins</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">18.25% </span>
<span class="quiet">Statements</span>
<span class='fraction'>426/2334</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">89.28% </span>
<span class="quiet">Branches</span>
<span class='fraction'>25/28</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">7.47% </span>
<span class="quiet">Functions</span>
<span class='fraction'>8/107</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">18.25% </span>
<span class="quiet">Lines</span>
<span class='fraction'>426/2334</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line low'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody><tr>
<td class="file low" data-value="BehaviorPlugin.ts"><a href="BehaviorPlugin.ts.html">BehaviorPlugin.ts</a></td>
<td data-value="15.52" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 15%"></div><div class="cover-empty" style="width: 85%"></div></div>
</td>
<td data-value="15.52" class="pct low">15.52%</td>
<td data-value="335" class="abs low">52/335</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="0" class="abs high">0/0</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="13" class="abs low">0/13</td>
<td data-value="15.52" class="pct low">15.52%</td>
<td data-value="335" class="abs low">52/335</td>
</tr>
<tr>
<td class="file low" data-value="ErrorPlugin.ts"><a href="ErrorPlugin.ts.html">ErrorPlugin.ts</a></td>
<td data-value="17.39" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 17%"></div><div class="cover-empty" style="width: 83%"></div></div>
</td>
<td data-value="17.39" class="pct low">17.39%</td>
<td data-value="299" class="abs low">52/299</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="0" class="abs high">0/0</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="14" class="abs low">0/14</td>
<td data-value="17.39" class="pct low">17.39%</td>
<td data-value="299" class="abs low">52/299</td>
</tr>
<tr>
<td class="file low" data-value="InteractionPlugin.ts"><a href="InteractionPlugin.ts.html">InteractionPlugin.ts</a></td>
<td data-value="16.9" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 16%"></div><div class="cover-empty" style="width: 84%"></div></div>
</td>
<td data-value="16.9" class="pct low">16.9%</td>
<td data-value="207" class="abs low">35/207</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="0" class="abs high">0/0</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="10" class="abs low">0/10</td>
<td data-value="16.9" class="pct low">16.9%</td>
<td data-value="207" class="abs low">35/207</td>
</tr>
<tr>
<td class="file low" data-value="NetworkPlugin.ts"><a href="NetworkPlugin.ts.html">NetworkPlugin.ts</a></td>
<td data-value="14.32" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 14%"></div><div class="cover-empty" style="width: 86%"></div></div>
</td>
<td data-value="14.32" class="pct low">14.32%</td>
<td data-value="384" class="abs low">55/384</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="0" class="abs high">0/0</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="14" class="abs low">0/14</td>
<td data-value="14.32" class="pct low">14.32%</td>
<td data-value="384" class="abs low">55/384</td>
</tr>
<tr>
<td class="file low" data-value="OfflinePlugin.ts"><a href="OfflinePlugin.ts.html">OfflinePlugin.ts</a></td>
<td data-value="14.47" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 14%"></div><div class="cover-empty" style="width: 86%"></div></div>
</td>
<td data-value="14.47" class="pct low">14.47%</td>
<td data-value="304" class="abs low">44/304</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="0" class="abs high">0/0</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="15" class="abs low">0/15</td>
<td data-value="14.47" class="pct low">14.47%</td>
<td data-value="304" class="abs low">44/304</td>
</tr>
<tr>
<td class="file low" data-value="PerformancePlugin.ts"><a href="PerformancePlugin.ts.html">PerformancePlugin.ts</a></td>
<td data-value="12.83" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 12%"></div><div class="cover-empty" style="width: 88%"></div></div>
</td>
<td data-value="12.83" class="pct low">12.83%</td>
<td data-value="530" class="abs low">68/530</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="0" class="abs high">0/0</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="23" class="abs low">0/23</td>
<td data-value="12.83" class="pct low">12.83%</td>
<td data-value="530" class="abs low">68/530</td>
</tr>
<tr>
<td class="file low" data-value="RouterPlugin.ts"><a href="RouterPlugin.ts.html">RouterPlugin.ts</a></td>
<td data-value="14.36" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 14%"></div><div class="cover-empty" style="width: 86%"></div></div>
</td>
<td data-value="14.36" class="pct low">14.36%</td>
<td data-value="181" class="abs low">26/181</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="0" class="abs high">0/0</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="10" class="abs low">0/10</td>
<td data-value="14.36" class="pct low">14.36%</td>
<td data-value="181" class="abs low">26/181</td>
</tr>
<tr>
<td class="file high" data-value="SamplingPlugin.ts"><a href="SamplingPlugin.ts.html">SamplingPlugin.ts</a></td>
<td data-value="100" class="pic high">
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="94" class="abs high">94/94</td>
<td data-value="89.28" class="pct high">89.28%</td>
<td data-value="28" class="abs high">25/28</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="8" class="abs high">8/8</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="94" class="abs high">94/94</td>
</tr>
</tbody>
</table>
</div>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-06-25T14:49:54.914Z
</div>
<script src="../../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../../sorter.js"></script>
<script src="../../block-navigation.js"></script>
</body>
</html>

View File

@ -0,0 +1,382 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for src/utils/baggage.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../../index.html">All files</a> / <a href="index.html">src/utils</a> baggage.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">97.97% </span>
<span class="quiet">Statements</span>
<span class='fraction'>97/99</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">97.14% </span>
<span class="quiet">Branches</span>
<span class='fraction'>34/35</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>3/3</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">97.97% </span>
<span class="quiet">Lines</span>
<span class='fraction'>97/99</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">38x</span>
<span class="cline-any cline-yes">36x</span>
<span class="cline-any cline-yes">38x</span>
<span class="cline-any cline-yes">38x</span>
<span class="cline-any cline-yes">38x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import type { BaggageFields } from '../types';
&nbsp;
const SENTRY_PREFIX = 'sentry-';
const MAX_BAGGAGE_LENGTH = 8192;
&nbsp;
export function serializeBaggage(fields: BaggageFields): string {
const parts: string[] = [];
&nbsp;
for (const [key, value] of Object.entries(fields)) {
if (value === undefined || value === null || value === '') continue;
&nbsp;
const prefixedKey = key.startsWith(SENTRY_PREFIX) ? key : `${SENTRY_PREFIX}${key}`;
parts.push(`${prefixedKey}=${encodeURIComponent(String(value))}`);
}
&nbsp;
let result = parts.join(',');
if (result.length &gt; MAX_BAGGAGE_LENGTH) {
result = result.substring(0, MAX_BAGGAGE_LENGTH);
const lastComma = result.lastIndexOf(',');
if (lastComma &gt; 0) <span class="branch-0 cbranch-no" title="branch not covered" >{</span>
<span class="cstat-no" title="statement not covered" > result = result.substring(0, lastComma);</span>
<span class="cstat-no" title="statement not covered" > }</span>
}
&nbsp;
return result;
}
&nbsp;
export function parseBaggage(header: string): BaggageFields {
const fields: BaggageFields = {};
&nbsp;
if (!header) return fields;
&nbsp;
const parts = header.split(',');
for (const part of parts) {
const eqIdx = part.indexOf('=');
if (eqIdx === -1) continue;
&nbsp;
const rawKey = part.substring(0, eqIdx).trim();
const value = decodeURIComponent(part.substring(eqIdx + 1).trim());
&nbsp;
let key = rawKey;
if (rawKey.startsWith(SENTRY_PREFIX)) {
key = rawKey.substring(SENTRY_PREFIX.length);
}
&nbsp;
if (key) {
fields[key] = value;
}
}
&nbsp;
return fields;
}
&nbsp;
export function createBaggageFromTransaction(
traceId: string,
options?: {
dsnPublicKey?: string;
sampled?: boolean;
sampleRate?: number;
user?: { id?: string; segment?: string };
release?: string;
environment?: string;
}
): BaggageFields {
const fields: BaggageFields = {
trace_id: traceId,
};
&nbsp;
if (options?.dsnPublicKey) {
fields.public_key = options.dsnPublicKey;
}
&nbsp;
if (options?.sampled !== undefined) {
fields.sampled = options.sampled ? 'true' : 'false';
}
&nbsp;
if (options?.sampleRate !== undefined) {
fields.sample_rate = String(options.sampleRate);
}
&nbsp;
if (options?.user) {
if (options.user.id) {
fields.user_id = options.user.id;
}
if (options.user.segment) {
fields.user_segment = options.user.segment;
}
}
&nbsp;
if (options?.release) {
fields.release = options.release;
}
&nbsp;
if (options?.environment) {
fields.environment = options.environment;
}
&nbsp;
return fields;
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-06-25T14:49:54.914Z
</div>
<script src="../../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../../sorter.js"></script>
<script src="../../block-navigation.js"></script>
</body>
</html>

View File

@ -0,0 +1,172 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for src/utils/dsn.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../../index.html">All files</a> / <a href="index.html">src/utils</a> dsn.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">93.1% </span>
<span class="quiet">Statements</span>
<span class='fraction'>27/29</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>5/5</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">66.66% </span>
<span class="quiet">Functions</span>
<span class='fraction'>2/3</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">93.1% </span>
<span class="quiet">Lines</span>
<span class='fraction'>27/29</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import type { DSNInfo } from '../types';
&nbsp;
export function parseDSN(dsn: string): DSNInfo {
try {
const match = dsn.match(/^(https?):\/\/([^@]+)@([^/]+)\/(.+)$/);
if (!match) {
throw new Error('Invalid DSN format');
}
&nbsp;
const [, protocol, publicKey, host, projectId] = match;
&nbsp;
return {
protocol,
publicKey,
host,
projectId,
};
} catch (e) {
throw new Error(`Failed to parse DSN: ${(e as Error).message}`);
}
}
&nbsp;
export function getEnvelopeUrl(dsn: DSNInfo): string {
return `${dsn.protocol}://${dsn.host}/api/${dsn.projectId}/envelope/?sentry_key=${dsn.publicKey}`;
}
&nbsp;
export <span class="fstat-no" title="function not covered" >function getStoreUrl(dsn: DSNInfo): string {</span>
<span class="cstat-no" title="statement not covered" > return `${dsn.protocol}://${dsn.host}/api/${dsn.projectId}/store/?sentry_key=${dsn.publicKey}`;</span>
<span class="cstat-no" title="statement not covered" >}</span>
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-06-25T14:49:54.914Z
</div>
<script src="../../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../../sorter.js"></script>
<script src="../../block-navigation.js"></script>
</body>
</html>

View File

@ -0,0 +1,964 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for src/utils/env.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../../index.html">All files</a> / <a href="index.html">src/utils</a> env.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>293/293</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">78.87% </span>
<span class="quiet">Branches</span>
<span class='fraction'>56/71</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>10/10</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>293/293</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a>
<a name='L103'></a><a href='#L103'>103</a>
<a name='L104'></a><a href='#L104'>104</a>
<a name='L105'></a><a href='#L105'>105</a>
<a name='L106'></a><a href='#L106'>106</a>
<a name='L107'></a><a href='#L107'>107</a>
<a name='L108'></a><a href='#L108'>108</a>
<a name='L109'></a><a href='#L109'>109</a>
<a name='L110'></a><a href='#L110'>110</a>
<a name='L111'></a><a href='#L111'>111</a>
<a name='L112'></a><a href='#L112'>112</a>
<a name='L113'></a><a href='#L113'>113</a>
<a name='L114'></a><a href='#L114'>114</a>
<a name='L115'></a><a href='#L115'>115</a>
<a name='L116'></a><a href='#L116'>116</a>
<a name='L117'></a><a href='#L117'>117</a>
<a name='L118'></a><a href='#L118'>118</a>
<a name='L119'></a><a href='#L119'>119</a>
<a name='L120'></a><a href='#L120'>120</a>
<a name='L121'></a><a href='#L121'>121</a>
<a name='L122'></a><a href='#L122'>122</a>
<a name='L123'></a><a href='#L123'>123</a>
<a name='L124'></a><a href='#L124'>124</a>
<a name='L125'></a><a href='#L125'>125</a>
<a name='L126'></a><a href='#L126'>126</a>
<a name='L127'></a><a href='#L127'>127</a>
<a name='L128'></a><a href='#L128'>128</a>
<a name='L129'></a><a href='#L129'>129</a>
<a name='L130'></a><a href='#L130'>130</a>
<a name='L131'></a><a href='#L131'>131</a>
<a name='L132'></a><a href='#L132'>132</a>
<a name='L133'></a><a href='#L133'>133</a>
<a name='L134'></a><a href='#L134'>134</a>
<a name='L135'></a><a href='#L135'>135</a>
<a name='L136'></a><a href='#L136'>136</a>
<a name='L137'></a><a href='#L137'>137</a>
<a name='L138'></a><a href='#L138'>138</a>
<a name='L139'></a><a href='#L139'>139</a>
<a name='L140'></a><a href='#L140'>140</a>
<a name='L141'></a><a href='#L141'>141</a>
<a name='L142'></a><a href='#L142'>142</a>
<a name='L143'></a><a href='#L143'>143</a>
<a name='L144'></a><a href='#L144'>144</a>
<a name='L145'></a><a href='#L145'>145</a>
<a name='L146'></a><a href='#L146'>146</a>
<a name='L147'></a><a href='#L147'>147</a>
<a name='L148'></a><a href='#L148'>148</a>
<a name='L149'></a><a href='#L149'>149</a>
<a name='L150'></a><a href='#L150'>150</a>
<a name='L151'></a><a href='#L151'>151</a>
<a name='L152'></a><a href='#L152'>152</a>
<a name='L153'></a><a href='#L153'>153</a>
<a name='L154'></a><a href='#L154'>154</a>
<a name='L155'></a><a href='#L155'>155</a>
<a name='L156'></a><a href='#L156'>156</a>
<a name='L157'></a><a href='#L157'>157</a>
<a name='L158'></a><a href='#L158'>158</a>
<a name='L159'></a><a href='#L159'>159</a>
<a name='L160'></a><a href='#L160'>160</a>
<a name='L161'></a><a href='#L161'>161</a>
<a name='L162'></a><a href='#L162'>162</a>
<a name='L163'></a><a href='#L163'>163</a>
<a name='L164'></a><a href='#L164'>164</a>
<a name='L165'></a><a href='#L165'>165</a>
<a name='L166'></a><a href='#L166'>166</a>
<a name='L167'></a><a href='#L167'>167</a>
<a name='L168'></a><a href='#L168'>168</a>
<a name='L169'></a><a href='#L169'>169</a>
<a name='L170'></a><a href='#L170'>170</a>
<a name='L171'></a><a href='#L171'>171</a>
<a name='L172'></a><a href='#L172'>172</a>
<a name='L173'></a><a href='#L173'>173</a>
<a name='L174'></a><a href='#L174'>174</a>
<a name='L175'></a><a href='#L175'>175</a>
<a name='L176'></a><a href='#L176'>176</a>
<a name='L177'></a><a href='#L177'>177</a>
<a name='L178'></a><a href='#L178'>178</a>
<a name='L179'></a><a href='#L179'>179</a>
<a name='L180'></a><a href='#L180'>180</a>
<a name='L181'></a><a href='#L181'>181</a>
<a name='L182'></a><a href='#L182'>182</a>
<a name='L183'></a><a href='#L183'>183</a>
<a name='L184'></a><a href='#L184'>184</a>
<a name='L185'></a><a href='#L185'>185</a>
<a name='L186'></a><a href='#L186'>186</a>
<a name='L187'></a><a href='#L187'>187</a>
<a name='L188'></a><a href='#L188'>188</a>
<a name='L189'></a><a href='#L189'>189</a>
<a name='L190'></a><a href='#L190'>190</a>
<a name='L191'></a><a href='#L191'>191</a>
<a name='L192'></a><a href='#L192'>192</a>
<a name='L193'></a><a href='#L193'>193</a>
<a name='L194'></a><a href='#L194'>194</a>
<a name='L195'></a><a href='#L195'>195</a>
<a name='L196'></a><a href='#L196'>196</a>
<a name='L197'></a><a href='#L197'>197</a>
<a name='L198'></a><a href='#L198'>198</a>
<a name='L199'></a><a href='#L199'>199</a>
<a name='L200'></a><a href='#L200'>200</a>
<a name='L201'></a><a href='#L201'>201</a>
<a name='L202'></a><a href='#L202'>202</a>
<a name='L203'></a><a href='#L203'>203</a>
<a name='L204'></a><a href='#L204'>204</a>
<a name='L205'></a><a href='#L205'>205</a>
<a name='L206'></a><a href='#L206'>206</a>
<a name='L207'></a><a href='#L207'>207</a>
<a name='L208'></a><a href='#L208'>208</a>
<a name='L209'></a><a href='#L209'>209</a>
<a name='L210'></a><a href='#L210'>210</a>
<a name='L211'></a><a href='#L211'>211</a>
<a name='L212'></a><a href='#L212'>212</a>
<a name='L213'></a><a href='#L213'>213</a>
<a name='L214'></a><a href='#L214'>214</a>
<a name='L215'></a><a href='#L215'>215</a>
<a name='L216'></a><a href='#L216'>216</a>
<a name='L217'></a><a href='#L217'>217</a>
<a name='L218'></a><a href='#L218'>218</a>
<a name='L219'></a><a href='#L219'>219</a>
<a name='L220'></a><a href='#L220'>220</a>
<a name='L221'></a><a href='#L221'>221</a>
<a name='L222'></a><a href='#L222'>222</a>
<a name='L223'></a><a href='#L223'>223</a>
<a name='L224'></a><a href='#L224'>224</a>
<a name='L225'></a><a href='#L225'>225</a>
<a name='L226'></a><a href='#L226'>226</a>
<a name='L227'></a><a href='#L227'>227</a>
<a name='L228'></a><a href='#L228'>228</a>
<a name='L229'></a><a href='#L229'>229</a>
<a name='L230'></a><a href='#L230'>230</a>
<a name='L231'></a><a href='#L231'>231</a>
<a name='L232'></a><a href='#L232'>232</a>
<a name='L233'></a><a href='#L233'>233</a>
<a name='L234'></a><a href='#L234'>234</a>
<a name='L235'></a><a href='#L235'>235</a>
<a name='L236'></a><a href='#L236'>236</a>
<a name='L237'></a><a href='#L237'>237</a>
<a name='L238'></a><a href='#L238'>238</a>
<a name='L239'></a><a href='#L239'>239</a>
<a name='L240'></a><a href='#L240'>240</a>
<a name='L241'></a><a href='#L241'>241</a>
<a name='L242'></a><a href='#L242'>242</a>
<a name='L243'></a><a href='#L243'>243</a>
<a name='L244'></a><a href='#L244'>244</a>
<a name='L245'></a><a href='#L245'>245</a>
<a name='L246'></a><a href='#L246'>246</a>
<a name='L247'></a><a href='#L247'>247</a>
<a name='L248'></a><a href='#L248'>248</a>
<a name='L249'></a><a href='#L249'>249</a>
<a name='L250'></a><a href='#L250'>250</a>
<a name='L251'></a><a href='#L251'>251</a>
<a name='L252'></a><a href='#L252'>252</a>
<a name='L253'></a><a href='#L253'>253</a>
<a name='L254'></a><a href='#L254'>254</a>
<a name='L255'></a><a href='#L255'>255</a>
<a name='L256'></a><a href='#L256'>256</a>
<a name='L257'></a><a href='#L257'>257</a>
<a name='L258'></a><a href='#L258'>258</a>
<a name='L259'></a><a href='#L259'>259</a>
<a name='L260'></a><a href='#L260'>260</a>
<a name='L261'></a><a href='#L261'>261</a>
<a name='L262'></a><a href='#L262'>262</a>
<a name='L263'></a><a href='#L263'>263</a>
<a name='L264'></a><a href='#L264'>264</a>
<a name='L265'></a><a href='#L265'>265</a>
<a name='L266'></a><a href='#L266'>266</a>
<a name='L267'></a><a href='#L267'>267</a>
<a name='L268'></a><a href='#L268'>268</a>
<a name='L269'></a><a href='#L269'>269</a>
<a name='L270'></a><a href='#L270'>270</a>
<a name='L271'></a><a href='#L271'>271</a>
<a name='L272'></a><a href='#L272'>272</a>
<a name='L273'></a><a href='#L273'>273</a>
<a name='L274'></a><a href='#L274'>274</a>
<a name='L275'></a><a href='#L275'>275</a>
<a name='L276'></a><a href='#L276'>276</a>
<a name='L277'></a><a href='#L277'>277</a>
<a name='L278'></a><a href='#L278'>278</a>
<a name='L279'></a><a href='#L279'>279</a>
<a name='L280'></a><a href='#L280'>280</a>
<a name='L281'></a><a href='#L281'>281</a>
<a name='L282'></a><a href='#L282'>282</a>
<a name='L283'></a><a href='#L283'>283</a>
<a name='L284'></a><a href='#L284'>284</a>
<a name='L285'></a><a href='#L285'>285</a>
<a name='L286'></a><a href='#L286'>286</a>
<a name='L287'></a><a href='#L287'>287</a>
<a name='L288'></a><a href='#L288'>288</a>
<a name='L289'></a><a href='#L289'>289</a>
<a name='L290'></a><a href='#L290'>290</a>
<a name='L291'></a><a href='#L291'>291</a>
<a name='L292'></a><a href='#L292'>292</a>
<a name='L293'></a><a href='#L293'>293</a>
<a name='L294'></a><a href='#L294'>294</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">293x</span>
<span class="cline-any cline-yes">293x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">293x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">219x</span>
<span class="cline-any cline-yes">219x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">219x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">28x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">23x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">12x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { md5 } from './hash';
&nbsp;
export interface BrowserInfo {
name: string;
version: string;
major: string;
}
&nbsp;
export interface OSInfo {
name: string;
version: string;
}
&nbsp;
export interface DeviceInfo {
type: 'desktop' | 'mobile' | 'tablet';
vendor: string;
model: string;
}
&nbsp;
export interface EnvInfo {
browser: BrowserInfo;
os: OSInfo;
device: DeviceInfo;
url: string;
referrer: string;
title: string;
language: string;
user_agent: string;
screen_width: number;
screen_height: number;
viewport_width: number;
viewport_height: number;
}
&nbsp;
function detectBrowser(ua: string): BrowserInfo {
const browsers: { name: string; pattern: RegExp; versionIndex?: number }[] = [
{ name: 'WeChat', pattern: /MicroMessenger\/([\d.]+)/i },
{ name: 'QQ Browser', pattern: /QIBrowser\/([\d.]+)/i },
{ name: 'UC Browser', pattern: /UCBrowser\/([\d.]+)/i },
{ name: 'Edge', pattern: /Edg\/([\d.]+)/i },
{ name: 'Edge', pattern: /Edge\/([\d.]+)/i },
{ name: 'Opera', pattern: /OPR\/([\d.]+)/i },
{ name: 'Opera', pattern: /Opera\/([\d.]+)/i },
{ name: 'Firefox', pattern: /Firefox\/([\d.]+)/i },
{ name: 'Safari', pattern: /Version\/([\d.]+).*Safari/i },
{ name: 'Chrome', pattern: /Chrome\/([\d.]+)/i },
{ name: 'IE', pattern: /MSIE ([\d.]+)/i },
{ name: 'IE', pattern: /Trident.*rv:([\d.]+)/i },
];
&nbsp;
for (const b of browsers) {
const match = ua.match(b.pattern);
if (match) {
const version = match[1<span class="branch-0 cbranch-no" title="branch not covered" >] || '';</span>
const major = version.split('.')[0<span class="branch-0 cbranch-no" title="branch not covered" >] || '';</span>
return { name: b.name, version, major };
}
}
&nbsp;
return { name: 'Unknown', version: '', major: '' };
}
&nbsp;
function detectOS(ua: string): OSInfo {
const oss: { name: string; pattern: RegExp; versionIndex?: number }[] = [
{ name: 'Windows 11', pattern: /Windows NT 10\.0.*Win64.*x64/i },
{ name: 'Windows 10', pattern: /Windows NT 10\.0/i },
{ name: 'Windows 8.1', pattern: /Windows NT 6\.3/i },
{ name: 'Windows 8', pattern: /Windows NT 6\.2/i },
{ name: 'Windows 7', pattern: /Windows NT 6\.1/i },
{ name: 'Windows Vista', pattern: /Windows NT 6\.0/i },
{ name: 'Windows XP', pattern: /Windows NT 5\.1/i },
{ name: 'macOS', pattern: /Mac OS X ([\d_.]+)/i },
{ name: 'iOS', pattern: /OS ([\d_.]+) like Mac OS X/i },
{ name: 'Android', pattern: /Android ([\d.]+)/i },
{ name: 'Android', pattern: /Android/i },
{ name: 'Linux', pattern: /Linux/i },
];
&nbsp;
for (const o of oss) {
const match = ua.match(o.pattern);
if (match) {
let version = match[1] || '';
if (version) {
version = version.replace(/_/g, '.');
}
return { name: o.name, version };
}
}
&nbsp;
return { name: 'Unknown', version: '' };
}
&nbsp;
function detectDevice(ua: string): DeviceInfo {
const isMobile = /Mobile|Android|iPhone|iPod|BlackBerry|Windows Phone|Opera Mini/i.test(ua);
const isTablet = /Tablet|iPad|PlayBook|Kindle|Silk/i.test(ua);
&nbsp;
let type: 'desktop' | 'mobile' | 'tablet' = 'desktop';
if (isTablet) {
type = 'tablet';
} else if (isMobile) {
type = 'mobile';
}
&nbsp;
let vendor = '';
let model = '';
&nbsp;
const iphoneMatch = ua.match(/iPhone/i);
const ipadMatch = ua.match(/iPad/i);
const samsungMatch = ua.match(/SM-([A-Z0-9]+)/i);
const pixelMatch = ua.match(/Pixel ([0-9A-Z]+)/i);
const huaweiMatch = ua.match(/HUAWEI ([A-Z0-9]+)/i);
&nbsp;
if (iphoneMatch) {
vendor = 'Apple';
model = 'iPhone';
} else if (ipadMatch) {
vendor = 'Apple';
model = 'iPad';
} else if (samsungMatch) {
vendor = 'Samsung';
model = samsungMatch[1];
} else if (pixelMatch) {
vendor = 'Google';
model = `Pixel ${pixelMatch[1]}`;
} else if (huaweiMatch) {
vendor = 'Huawei';
model = huaweiMatch[1];
}
&nbsp;
return { type, vendor, model };
}
&nbsp;
export function getEnvInfo(): EnvInfo {
const ua = typeof navigator !== 'undefined' ? navigator.<span class="branch-0 cbranch-no" title="branch not covered" >userAgent : '';</span>
const browser = detectBrowser(ua);
const os = detectOS(ua);
const device = detectDevice(ua);
&nbsp;
return {
browser,
os,
device,
url: typeof location !== 'undefined' ? location.<span class="branch-0 cbranch-no" title="branch not covered" >href : '',</span>
referrer: typeof document !== 'undefined' ? document.<span class="branch-0 cbranch-no" title="branch not covered" >referrer : '',</span>
title: typeof document !== 'undefined' ? document.<span class="branch-0 cbranch-no" title="branch not covered" >title : '',</span>
language: typeof navigator !== 'undefined' ? navigator.<span class="branch-0 cbranch-no" title="branch not covered" >language : '',</span>
user_agent: ua,
screen_width: typeof screen !== 'undefined' ? screen.<span class="branch-0 cbranch-no" title="branch not covered" >width : 0,</span>
screen_height: typeof screen !== 'undefined' ? screen.<span class="branch-0 cbranch-no" title="branch not covered" >height : 0,</span>
viewport_width: typeof window !== 'undefined' ? window.<span class="branch-0 cbranch-no" title="branch not covered" >innerWidth : 0,</span>
viewport_height: typeof window !== 'undefined' ? window.<span class="branch-0 cbranch-no" title="branch not covered" >innerHeight : 0,</span>
};
}
&nbsp;
export function getPageUrl(): string {
return typeof location !== 'undefined' ? location.<span class="branch-0 cbranch-no" title="branch not covered" >href : '';</span>
}
&nbsp;
export function getReferrer(): string {
return typeof document !== 'undefined' ? document.<span class="branch-0 cbranch-no" title="branch not covered" >referrer : '';</span>
}
&nbsp;
export function sanitizeUrl(url: string): string {
if (!url) return url;
&nbsp;
// 过滤 javascript: 伪协议防止 XSS
if (/^\s*javascript:/i.test(url)) {
return 'javascript:[filtered]';
}
&nbsp;
try {
const urlObj = new URL(url);
const sensitiveParams = [
'password', 'passwd', 'pwd',
'token', 'access_token', 'refresh_token',
'api_key', 'apikey', 'key',
'secret', 'private_key',
'code', 'auth',
'id_token', 'idToken',
'session', 'session_id',
];
&nbsp;
const params = urlObj.searchParams;
let modified = false;
&nbsp;
for (const key of Array.from(params.keys())) {
const lowerKey = key.toLowerCase().replace(/[-_]/g, '_');
if (sensitiveParams.some(s =&gt; lowerKey.includes(s))) {
params.set(key, '[Filtered]');
modified = true;
}
}
&nbsp;
if (urlObj.hash &amp;&amp; urlObj.hash.length &gt; 1) {
const hashContent = urlObj.hash.substring(1);
if (hashContent.includes('=')) {
const hashParams = new URLSearchParams(hashContent);
let hashModified = false;
for (const key of Array.from(hashParams.keys())) {
const lowerKey = key.toLowerCase().replace(/[-_]/g, '_');
if (sensitiveParams.some(s =&gt; lowerKey.includes(s))) {
hashParams.set(key, '[Filtered]');
hashModified = true;
}
}
if (hashModified) {
urlObj.hash = '#' + hashParams.toString();
modified = true;
}
}
}
&nbsp;
if (modified) {
return urlObj.toString();
}
return url;
} catch {
return url;
}
}
&nbsp;
export function shouldIgnoreUrl(url: string, ignoreUrls: (string | RegExp)[] = []): boolean {
if (!ignoreUrls || ignoreUrls.length === 0) return false;
&nbsp;
return ignoreUrls.some(pattern =&gt; {
if (typeof pattern === 'string') {
if (pattern.includes('*')) {
// 移除开头的 ^ 和结尾的 $,允许匹配 URL 的任意部分
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&amp;').replace(/\*/g, '.*');
const regex = new RegExp(escaped);
return regex.test(url);
}
return url.includes(pattern);
}
return pattern.test(url);
});
}
&nbsp;
const BROWSER_CODE_MAP: Record&lt;string, string&gt; = {
'Chrome': 'c',
'Safari': 's',
'Firefox': 'f',
'Edge': 'e',
'Opera': 'o',
'IE': 'i',
'WeChat': 'w',
'QQ Browser': 'q',
'UC Browser': 'u',
'Mobile Chrome': 'cm',
'Mobile Safari': 'sm',
};
&nbsp;
const OS_CODE_MAP: Record&lt;string, string&gt; = {
'Windows 11': 'w11',
'Windows 10': 'w10',
'Windows 8.1': 'w81',
'Windows 8': 'w8',
'Windows 7': 'w7',
'Windows Vista': 'wv',
'Windows XP': 'wxp',
'macOS': 'm',
'iOS': 'i',
'Android': 'a',
'Linux': 'l',
};
&nbsp;
export function getContexts() {
const env = getEnvInfo();
const browserCode = BROWSER_CODE_MAP[env.browser.name<span class="branch-0 cbranch-no" title="branch not covered" >] || env.browser.name.toLowerCase();</span>
const osCode = OS_CODE_MAP[env.os.name<span class="branch-0 cbranch-no" title="branch not covered" >] || env.os.name.toLowerCase();</span>
&nbsp;
return {
browser: {
name: browserCode,
version: env.browser.major,
},
os: {
name: osCode,
version: env.os.version.split('.')[0],
},
device: {
type: env.device.type,
brand: env.device.vendor || undefined,
model: env.device.model || undefined,
},
};
}
&nbsp;
export function getContextId(): string {
const env = getEnvInfo();
const key = `${env.browser.name}|${env.browser.major}|${env.os.name}|${env.os.version.split('.')[0]}|${env.device.type}|${env.device.vendor}|${env.device.model}|${env.language}`;
return md5(key).substring(0, 16);
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-06-25T14:49:54.914Z
</div>
<script src="../../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../../sorter.js"></script>
<script src="../../block-navigation.js"></script>
</body>
</html>

View File

@ -0,0 +1,349 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for src/utils/field-encoder.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../../index.html">All files</a> / <a href="index.html">src/utils</a> field-encoder.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>88/88</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>15/15</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>2/2</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>88/88</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">23x</span>
<span class="cline-any cline-yes">23x</span>
<span class="cline-any cline-yes">23x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">/**
* 字段短编码映射
*
* 将常用字段名编码为 2-3 个字符的短码,减少上报体积
*
* 编码前:{ "type": "error", "level": "error", "message": "..." }
* 编码后:{ "t": "e", "l": "e", "m": "..." }
*
* 注意:只对最外层高频字段编码,不对嵌套对象深度编码
* 避免复杂嵌套带来的解析问题和兼容性风险
*/
&nbsp;
// SDK 端:字段名编码映射(只包含最外层高频字段)
export const FIELD_ENCODE_MAP: Record&lt;string, string&gt; = {
// 基础字段
event_id: 'eid',
timestamp: 'ts',
start_timestamp: 'sts',
type: 't',
level: 'l',
message: 'm',
platform: 'p',
release: 'r',
environment: 'e',
fingerprint: 'fp',
context_id: 'cid',
&nbsp;
// 用户
user: 'u',
&nbsp;
// 标签
tags: 'tg',
&nbsp;
// 异常
exception: 'ex',
&nbsp;
// 请求
request: 'req',
&nbsp;
// 上下文
contexts: 'ctx',
&nbsp;
// 面包屑
breadcrumbs: 'bc',
&nbsp;
// Transaction
transaction: 'tx',
duration: 'd',
spans: 'sp',
&nbsp;
// 额外信息
extra: 'xt',
};
&nbsp;
// 服务端:字段名解码映射(反向)
export const FIELD_DECODE_MAP: Record&lt;string, string&gt; = Object.fromEntries(
Object.entries(FIELD_ENCODE_MAP).map(([k, v]) =&gt; [v, k])
);
&nbsp;
/**
* 编码事件字段名SDK 端使用)
* 只对最外层字段编码,避免深度编码的复杂性
*/
export function encodeFields(obj: Record&lt;string, unknown&gt;): Record&lt;string, unknown&gt; {
if (!obj || typeof obj !== 'object') return obj;
&nbsp;
const result: Record&lt;string, unknown&gt; = {};
for (const [key, value] of Object.entries(obj)) {
const shortKey = FIELD_ENCODE_MAP[key] || key;
result[shortKey] = value;
}
return result;
}
&nbsp;
/**
* 解码事件字段名(服务端使用)
* 只对最外层字段解码
*/
export function decodeFields(obj: Record&lt;string, unknown&gt;): Record&lt;string, unknown&gt; {
if (!obj || typeof obj !== 'object') return obj;
&nbsp;
const result: Record&lt;string, unknown&gt; = {};
for (const [key, value] of Object.entries(obj)) {
const fullKey = FIELD_DECODE_MAP[key] || key;
result[fullKey] = value;
}
return result;
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-06-25T14:49:54.914Z
</div>
<script src="../../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../../sorter.js"></script>
<script src="../../block-navigation.js"></script>
</body>
</html>

View File

@ -0,0 +1,145 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for src/utils/hash.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../../index.html">All files</a> / <a href="index.html">src/utils</a> hash.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>20/20</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">83.33% </span>
<span class="quiet">Branches</span>
<span class='fraction'>5/6</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>3/3</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>20/20</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">288x</span>
<span class="cline-any cline-yes">288x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">export function hashString(str: string): number {
let hash = 5381;
let i = str.length;
while (i) {
hash = (hash * 33) ^ str.charCodeAt(--i);
}
return hash &gt;&gt;&gt; 0;
}
&nbsp;
export function md5(str: string): string {
// 使用 hashString 替代不安全的 md5
// 用于非加密场景ID生成、指纹计算
return hashString(str).toString(16);
}
&nbsp;
export function computeFingerprint(type: string, message: string, frames: { filename: string; lineno?: number }[] = []): string {
const keyFrames = frames.slice(0, 3).map(f =&gt; `${f.filename}:${f.<span class="branch-0 cbranch-no" title="branch not covered" >lineno || 0}</span>`).join('|');
const normalizedMessage = message.replace(/['"]?\d+['"]?/g, '').replace(/\s+/g, ' ').trim();
return md5(`${type}:${normalizedMessage}:${keyFrames}`);
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-06-25T14:49:54.914Z
</div>
<script src="../../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../../sorter.js"></script>
<script src="../../block-navigation.js"></script>
</body>
</html>

View File

@ -0,0 +1,241 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for src/utils/helper.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../../index.html">All files</a> / <a href="index.html">src/utils</a> helper.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">96.15% </span>
<span class="quiet">Statements</span>
<span class='fraction'>50/52</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">95.83% </span>
<span class="quiet">Branches</span>
<span class='fraction'>23/24</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>9/9</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">96.15% </span>
<span class="quiet">Lines</span>
<span class='fraction'>50/52</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">96x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">293x</span>
<span class="cline-any cline-yes">293x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">190x</span>
<span class="cline-any cline-yes">190x</span>
<span class="cline-any cline-yes">190x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">38x</span>
<span class="cline-any cline-yes">38x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">export function generateEventId(): string {
return 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.replace(/[x]/g, () =&gt; {
return ((Math.random() * 16) | 0).toString(16);
});
}
&nbsp;
export function now(): number {
return Date.now();
}
&nbsp;
export function uuid(): string {
if (typeof crypto !== 'undefined' &amp;&amp; crypto.randomUUID) {
return crypto.randomUUID().replace(/-/g, '');
<span class="branch-0 cbranch-no" title="branch not covered" > }</span>
<span class="cstat-no" title="statement not covered" > return generateEventId();</span>
<span class="cstat-no" title="statement not covered" >}</span>
&nbsp;
export function isFunction(fn: unknown): fn is (...args: unknown[]) =&gt; unknown {
return typeof fn === 'function';
}
&nbsp;
export function isString(value: unknown): value is string {
return typeof value === 'string';
}
&nbsp;
export function isObject(value: unknown): value is Record&lt;string, unknown&gt; {
return value !== null &amp;&amp; typeof value === 'object' &amp;&amp; !Array.isArray(value);
}
&nbsp;
export function safeGet&lt;T&gt;(fn: () =&gt; T, defaultValue: T): T {
try {
const result = fn();
return result === undefined ? defaultValue : result;
} catch {
return defaultValue;
}
}
&nbsp;
export function truncate(str: string, maxLen: number): string {
if (str.length &lt;= maxLen) return str;
return str.slice(0, maxLen) + '...';
}
&nbsp;
export function matchPatterns(value: string, patterns: (string | RegExp)[]): boolean {
if (!patterns || patterns.length === 0) return false;
return patterns.some(pattern =&gt; {
if (typeof pattern === 'string') {
return value.includes(pattern);
}
return pattern.test(value);
});
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-06-25T14:49:54.914Z
</div>
<script src="../../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../../sorter.js"></script>
<script src="../../block-navigation.js"></script>
</body>
</html>

View File

@ -0,0 +1,295 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for src/utils/identity.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../../index.html">All files</a> / <a href="index.html">src/utils</a> identity.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>70/70</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>15/15</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>4/4</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>70/70</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { uuid } from './helper';
&nbsp;
const ANONYMOUS_ID_KEY = 'light_sentry_aid';
&nbsp;
/**
* 获取匿名用户ID
* 优先从 localStorage 获取已存在的ID不存在则生成并存储
*/
export function getAnonymousId(): string | null {
// 浏览器环境检查
if (typeof localStorage === 'undefined') {
return null;
}
&nbsp;
try {
let anonymousId = localStorage.getItem(ANONYMOUS_ID_KEY);
&nbsp;
if (!anonymousId) {
anonymousId = uuid();
localStorage.setItem(ANONYMOUS_ID_KEY, anonymousId);
}
&nbsp;
return anonymousId;
} catch {
// localStorage 不可用(如隐私模式、跨域限制等)
return null;
}
}
&nbsp;
/**
* 设置匿名用户ID
* 用于覆盖自动生成的ID
*/
export function setAnonymousId(id: string): boolean {
if (typeof localStorage === 'undefined') {
return false;
}
&nbsp;
try {
localStorage.setItem(ANONYMOUS_ID_KEY, id);
return true;
} catch {
return false;
}
}
&nbsp;
/**
* 清除匿名用户ID
* 通常在用户登出后调用
*/
export function clearAnonymousId(): boolean {
if (typeof localStorage === 'undefined') {
return false;
}
&nbsp;
try {
localStorage.removeItem(ANONYMOUS_ID_KEY);
return true;
} catch {
return false;
}
}
&nbsp;
/**
* 检查是否启用了匿名用户追踪
*/
export function isAnonymousTrackingEnabled(config: { trackAnonymousUsers?: boolean }): boolean {
// 默认为 true除非明确设置为 false
return config.trackAnonymousUsers !== false;
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-06-25T14:49:54.914Z
</div>
<script src="../../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../../sorter.js"></script>
<script src="../../block-navigation.js"></script>
</body>
</html>

View File

@ -0,0 +1,251 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for src/utils</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../../index.html">All files</a> src/utils</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">97.36% </span>
<span class="quiet">Statements</span>
<span class='fraction'>1108/1138</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">87.87% </span>
<span class="quiet">Branches</span>
<span class='fraction'>319/363</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">96.92% </span>
<span class="quiet">Functions</span>
<span class='fraction'>63/65</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">97.36% </span>
<span class="quiet">Lines</span>
<span class='fraction'>1108/1138</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody><tr>
<td class="file high" data-value="baggage.ts"><a href="baggage.ts.html">baggage.ts</a></td>
<td data-value="97.97" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 97%"></div><div class="cover-empty" style="width: 3%"></div></div>
</td>
<td data-value="97.97" class="pct high">97.97%</td>
<td data-value="99" class="abs high">97/99</td>
<td data-value="97.14" class="pct high">97.14%</td>
<td data-value="35" class="abs high">34/35</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="3" class="abs high">3/3</td>
<td data-value="97.97" class="pct high">97.97%</td>
<td data-value="99" class="abs high">97/99</td>
</tr>
<tr>
<td class="file high" data-value="dsn.ts"><a href="dsn.ts.html">dsn.ts</a></td>
<td data-value="93.1" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 93%"></div><div class="cover-empty" style="width: 7%"></div></div>
</td>
<td data-value="93.1" class="pct high">93.1%</td>
<td data-value="29" class="abs high">27/29</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="5" class="abs high">5/5</td>
<td data-value="66.66" class="pct medium">66.66%</td>
<td data-value="3" class="abs medium">2/3</td>
<td data-value="93.1" class="pct high">93.1%</td>
<td data-value="29" class="abs high">27/29</td>
</tr>
<tr>
<td class="file high" data-value="env.ts"><a href="env.ts.html">env.ts</a></td>
<td data-value="100" class="pic high">
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="293" class="abs high">293/293</td>
<td data-value="78.87" class="pct medium">78.87%</td>
<td data-value="71" class="abs medium">56/71</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="10" class="abs high">10/10</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="293" class="abs high">293/293</td>
</tr>
<tr>
<td class="file high" data-value="field-encoder.ts"><a href="field-encoder.ts.html">field-encoder.ts</a></td>
<td data-value="100" class="pic high">
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="88" class="abs high">88/88</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="15" class="abs high">15/15</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="2" class="abs high">2/2</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="88" class="abs high">88/88</td>
</tr>
<tr>
<td class="file high" data-value="hash.ts"><a href="hash.ts.html">hash.ts</a></td>
<td data-value="100" class="pic high">
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="20" class="abs high">20/20</td>
<td data-value="83.33" class="pct high">83.33%</td>
<td data-value="6" class="abs high">5/6</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="3" class="abs high">3/3</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="20" class="abs high">20/20</td>
</tr>
<tr>
<td class="file high" data-value="helper.ts"><a href="helper.ts.html">helper.ts</a></td>
<td data-value="96.15" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 96%"></div><div class="cover-empty" style="width: 4%"></div></div>
</td>
<td data-value="96.15" class="pct high">96.15%</td>
<td data-value="52" class="abs high">50/52</td>
<td data-value="95.83" class="pct high">95.83%</td>
<td data-value="24" class="abs high">23/24</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="9" class="abs high">9/9</td>
<td data-value="96.15" class="pct high">96.15%</td>
<td data-value="52" class="abs high">50/52</td>
</tr>
<tr>
<td class="file high" data-value="identity.ts"><a href="identity.ts.html">identity.ts</a></td>
<td data-value="100" class="pic high">
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="70" class="abs high">70/70</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="15" class="abs high">15/15</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="4" class="abs high">4/4</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="70" class="abs high">70/70</td>
</tr>
<tr>
<td class="file high" data-value="logger.ts"><a href="logger.ts.html">logger.ts</a></td>
<td data-value="100" class="pic high">
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="31" class="abs high">31/31</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="13" class="abs high">13/13</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="4" class="abs high">4/4</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="31" class="abs high">31/31</td>
</tr>
<tr>
<td class="file high" data-value="stacktrace.ts"><a href="stacktrace.ts.html">stacktrace.ts</a></td>
<td data-value="98" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 98%"></div><div class="cover-empty" style="width: 2%"></div></div>
</td>
<td data-value="98" class="pct high">98%</td>
<td data-value="100" class="abs high">98/100</td>
<td data-value="93.02" class="pct high">93.02%</td>
<td data-value="43" class="abs high">40/43</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="1" class="abs high">1/1</td>
<td data-value="98" class="pct high">98%</td>
<td data-value="100" class="abs high">98/100</td>
</tr>
<tr>
<td class="file high" data-value="tracing.ts"><a href="tracing.ts.html">tracing.ts</a></td>
<td data-value="93.82" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 93%"></div><div class="cover-empty" style="width: 7%"></div></div>
</td>
<td data-value="93.82" class="pct high">93.82%</td>
<td data-value="356" class="abs high">334/356</td>
<td data-value="83.08" class="pct high">83.08%</td>
<td data-value="136" class="abs high">113/136</td>
<td data-value="96.15" class="pct high">96.15%</td>
<td data-value="26" class="abs high">25/26</td>
<td data-value="93.82" class="pct high">93.82%</td>
<td data-value="356" class="abs high">334/356</td>
</tr>
</tbody>
</table>
</div>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-06-25T14:49:54.914Z
</div>
<script src="../../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../../sorter.js"></script>
<script src="../../block-navigation.js"></script>
</body>
</html>

View File

@ -0,0 +1,178 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for src/utils/logger.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../../index.html">All files</a> / <a href="index.html">src/utils</a> logger.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>31/31</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>13/13</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>4/4</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>31/31</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">42x</span>
<span class="cline-any cline-yes">42x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">289x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">289x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">let debugEnabled = false;
&nbsp;
export function setDebug(enabled: boolean): void {
debugEnabled = enabled;
}
&nbsp;
export function isDebugEnabled(): boolean {
return debugEnabled;
}
&nbsp;
export function logDebug(tag: string, message: string, data?: unknown): void {
if (!debugEnabled) return;
const timestamp = new Date().toISOString();
const prefix = `[LightSDK][${tag}]`;
if (data !== undefined) {
console.log(`${prefix} ${timestamp} ${message}`, data);
} else {
console.log(`${prefix} ${timestamp} ${message}`);
}
}
&nbsp;
export function logError(tag: string, message: string, error?: unknown): void {
if (!debugEnabled) return;
const timestamp = new Date().toISOString();
const prefix = `[LightSDK][${tag}][ERROR]`;
if (error !== undefined) {
console.error(`${prefix} ${timestamp} ${message}`, error);
} else {
console.error(`${prefix} ${timestamp} ${message}`);
}
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-06-25T14:49:54.914Z
</div>
<script src="../../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../../sorter.js"></script>
<script src="../../block-navigation.js"></script>
</body>
</html>

View File

@ -0,0 +1,385 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for src/utils/stacktrace.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="../../prettify.css" />
<link rel="stylesheet" href="../../base.css" />
<link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(../../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="../../index.html">All files</a> / <a href="index.html">src/utils</a> stacktrace.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">98% </span>
<span class="quiet">Statements</span>
<span class='fraction'>98/100</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">93.02% </span>
<span class="quiet">Branches</span>
<span class='fraction'>40/43</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>1/1</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">98% </span>
<span class="quiet">Lines</span>
<span class='fraction'>98/100</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">24x</span>
<span class="cline-any cline-yes">24x</span>
<span class="cline-any cline-yes">24x</span>
<span class="cline-any cline-yes">24x</span>
<span class="cline-any cline-yes">24x</span>
<span class="cline-any cline-yes">24x</span>
<span class="cline-any cline-yes">24x</span>
<span class="cline-any cline-yes">24x</span>
<span class="cline-any cline-yes">24x</span>
<span class="cline-any cline-yes">24x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">73x</span>
<span class="cline-any cline-yes">73x</span>
<span class="cline-any cline-yes">28x</span>
<span class="cline-any cline-yes">28x</span>
<span class="cline-any cline-yes">28x</span>
<span class="cline-any cline-yes">28x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">28x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">28x</span>
<span class="cline-any cline-yes">28x</span>
<span class="cline-any cline-yes">28x</span>
<span class="cline-any cline-yes">73x</span>
<span class="cline-any cline-yes">45x</span>
<span class="cline-any cline-yes">45x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">34x</span>
<span class="cline-any cline-yes">34x</span>
<span class="cline-any cline-yes">45x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">45x</span>
<span class="cline-any cline-yes">73x</span>
<span class="cline-any cline-yes">24x</span>
<span class="cline-any cline-yes">24x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">23x</span>
<span class="cline-any cline-yes">23x</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import type { StackFrame } from '../types';
&nbsp;
export interface ParseStackOptions {
captureNodeModules?: boolean;
captureColumn?: boolean;
relativePathOnly?: boolean;
maxFrames?: number;
}
&nbsp;
export function parseStackTrace(stack?: string, options: ParseStackOptions = {}): StackFrame[] {
if (!stack) return [];
&nbsp;
const {
captureNodeModules = false,
captureColumn = true,
relativePathOnly = false,
maxFrames,
} = options;
&nbsp;
const frames: StackFrame[] = [];
const lines = stack.split('\n');
const origin = typeof location !== 'undefined' ? `${location.protocol}//${location.host}` : '';
&nbsp;
for (const line of lines) {
const match = line.match(/^\s*at\s*(.+?)\s*\((.+?):(\d+):(\d+)\)\s*$/);
if (match) {
const [, fn, filename, lineNum, colNum] = match;
const isNodeModules = filename.includes('node_modules');
if (!captureNodeModules &amp;&amp; isNodeModules) {
continue;
}
&nbsp;
let finalFilename = filename;
if (relativePathOnly &amp;&amp; <span class="branch-0 cbranch-no" title="branch not covered" >origin &amp;&amp; filename.startsWith(origin)) <span class="branch-0 cbranch-no" title="branch not covered" >{</span></span>
<span class="cstat-no" title="statement not covered" > finalFilename = filename.substring(origin.length);</span>
<span class="cstat-no" title="statement not covered" > }</span>
&nbsp;
frames.push({
filename: finalFilename,
function: fn,
lineno: parseInt(lineNum, 10),
colno: captureColumn ? parseInt(colNum, 10) : undefined,
in_app: !isNodeModules,
});
} else {
const v8Match = line.match(/^\s*at\s*(.+?)@(.+?):(\d+):(\d+)\s*$/);
if (v8Match) {
const [, fn, filename, lineNum, colNum] = v8Match;
const isNodeModules = filename.includes('node_modules');
if (!captureNodeModules &amp;&amp; isNodeModules) {
continue;
}
&nbsp;
let finalFilename = filename;
if (relativePathOnly &amp;&amp; origin &amp;&amp; filename.startsWith(origin)) {
finalFilename = filename.substring(origin.length);
}
&nbsp;
frames.push({
filename: finalFilename,
function: <span class="branch-0 cbranch-no" title="branch not covered" >fn || '&lt;anonymous&gt;',</span>
lineno: parseInt(lineNum, 10),
colno: captureColumn ? parseInt(colNum, 10) : undefined,
in_app: !isNodeModules,
});
continue;
}
&nbsp;
const urlMatch = line.match(/^\s*at\s*(.+?):(\d+):(\d+)\s*$/);
if (urlMatch) {
const [, filename, lineNum, colNum] = urlMatch;
const isNodeModules = filename.includes('node_modules');
if (!captureNodeModules &amp;&amp; isNodeModules) {
continue;
}
&nbsp;
let finalFilename = filename;
if (relativePathOnly &amp;&amp; origin &amp;&amp; filename.startsWith(origin)) {
finalFilename = filename.substring(origin.length);
}
&nbsp;
frames.push({
filename: finalFilename,
lineno: parseInt(lineNum, 10),
colno: captureColumn ? parseInt(colNum, 10) : undefined,
in_app: !isNodeModules,
});
}
}
}
&nbsp;
const reversed = frames.reverse();
if (maxFrames !== undefined &amp;&amp; maxFrames &gt; 0) {
return reversed.slice(0, maxFrames);
}
return reversed;
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-06-25T14:49:54.914Z
</div>
<script src="../../prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="../../sorter.js"></script>
<script src="../../block-navigation.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

10
dist/core/Client.d.ts vendored
View File

@ -1,4 +1,4 @@
import type { LightConfig, LightClient, LightPlugin, DSNInfo, SentryEvent, EventLevel, UserInfo, Breadcrumb } from '../types'; import type { LightConfig, LightClient, LightPlugin, DSNInfo, SentryEvent, EventLevel, UserInfo, Breadcrumb, TransactionContext, SpanContext, Transaction, Span } from '../types';
declare class Client implements LightClient { declare class Client implements LightClient {
dsn: DSNInfo; dsn: DSNInfo;
private configManager; private configManager;
@ -6,6 +6,7 @@ declare class Client implements LightClient {
private queue; private queue;
private reporter; private reporter;
private pluginManager; private pluginManager;
private tracingManager;
private breadcrumbs; private breadcrumbs;
private enabled; private enabled;
private get maxBreadcrumbs(); private get maxBreadcrumbs();
@ -22,8 +23,15 @@ declare class Client implements LightClient {
captureEvent(event: Partial<SentryEvent> & { captureEvent(event: Partial<SentryEvent> & {
type: string; type: string;
}): void; }): void;
private attachTraceContext;
private buildErrorEvent; private buildErrorEvent;
setUser(user: UserInfo | null): void; setUser(user: UserInfo | null): void;
setAnonymousId(id: string): void;
startTransaction(context: TransactionContext): Transaction | null;
getCurrentTransaction(): Transaction | null;
getCurrentSpan(): Span | null;
setSpan(span: Span | null): void;
startSpan(context: SpanContext): Span | null;
setTag(key: string, value: string): void; setTag(key: string, value: string): void;
setTags(tags: Record<string, string>): void; setTags(tags: Record<string, string>): void;
setExtra(key: string, value: unknown): void; setExtra(key: string, value: unknown): void;

View File

@ -14,6 +14,7 @@ export declare class ConfigManager {
setTag(key: string, value: string): void; setTag(key: string, value: string): void;
setExtra(key: string, value: unknown): void; setExtra(key: string, value: unknown): void;
isIgnoredError(message: string): boolean; isIgnoredError(message: string): boolean;
shouldSample(): boolean;
/** /**
* *
* env.ts getContexts * env.ts getContexts

22
dist/core/TracingManager.d.ts vendored Normal file
View File

@ -0,0 +1,22 @@
import type { LightConfig, SpanContext, TransactionContext, Span, Transaction, LightClient } from '../types';
export declare class TracingManager {
private config;
private _client?;
private currentTransaction;
private spanStack;
private dsnPublicKey?;
constructor(config: LightConfig, client?: LightClient, dsnPublicKey?: string);
updateConfig(config: LightConfig): void;
private shouldSample;
startTransaction(context: TransactionContext): Transaction | null;
getCurrentTransaction(): Transaction | null;
setCurrentTransaction(transaction: Transaction | null): void;
getCurrentSpan(): Span | null;
setCurrentSpan(span: Span | null): void;
startSpan(context: SpanContext): Span | null;
onSpanFinished(span: Span): void;
onTransactionFinished(transaction: Transaction): void;
finishSpan(span: Span): void;
finishTransaction(transaction: Transaction): void;
destroy(): void;
}

6
dist/index.cjs.js vendored

File diff suppressed because one or more lines are too long

30
dist/index.d.ts vendored
View File

@ -5,7 +5,11 @@ import NetworkPlugin from './plugins/NetworkPlugin';
import BehaviorPlugin from './plugins/BehaviorPlugin'; import BehaviorPlugin from './plugins/BehaviorPlugin';
import OfflinePlugin from './plugins/OfflinePlugin'; import OfflinePlugin from './plugins/OfflinePlugin';
import SamplingPlugin from './plugins/SamplingPlugin'; import SamplingPlugin from './plugins/SamplingPlugin';
import type { LightConfig, LightClient, SentryEvent, EventLevel, UserInfo, Breadcrumb, LightPlugin, ErrorEvent, PerformanceEvent, NetworkEvent, BehaviorEvent, DSNInfo } from './types'; import RouterPlugin from './plugins/RouterPlugin';
import InteractionPlugin from './plugins/InteractionPlugin';
import { Span as SpanClass, Transaction as TransactionClass, httpStatusToSpanStatus, dbStatusToSpanStatus, statusToSpanStatus } from './utils/tracing';
import { serializeBaggage, parseBaggage, createBaggageFromTransaction } from './utils/baggage';
import type { LightConfig, LightClient, SentryEvent, EventLevel, UserInfo, Breadcrumb, LightPlugin, ErrorEvent, PerformanceEvent, NetworkEvent, BehaviorEvent, DSNInfo, TransactionContext, SpanContext, Transaction, Span, SamplingContext, BaggageFields, SpanStatus } from './types';
declare function init(config: LightConfig): LightClient; declare function init(config: LightConfig): LightClient;
declare function getClient(): LightClient | null; declare function getClient(): LightClient | null;
declare function captureException(error: Error | unknown): void; declare function captureException(error: Error | unknown): void;
@ -14,6 +18,14 @@ declare function captureEvent(event: Partial<SentryEvent> & {
type: string; type: string;
}): void; }): void;
declare function setUser(user: UserInfo | null): void; declare function setUser(user: UserInfo | null): void;
declare function setAnonymousId(id: string): void;
declare function clearAnonymousId(): void;
declare function getAnonymousId(): string | null;
declare function startTransaction(context: TransactionContext): Transaction | null;
declare function getCurrentTransaction(): Transaction | null;
declare function getCurrentSpan(): Span | null;
declare function setSpan(span: Span | null): void;
declare function startSpan(context: SpanContext): Span | null;
declare function setTag(key: string, value: string): void; declare function setTag(key: string, value: string): void;
declare function setTags(tags: Record<string, string>): void; declare function setTags(tags: Record<string, string>): void;
declare function setExtra(key: string, value: unknown): void; declare function setExtra(key: string, value: unknown): void;
@ -21,8 +33,9 @@ declare function addBreadcrumb(breadcrumb: Omit<Breadcrumb, 'timestamp'>): void;
declare function flush(): Promise<void>; declare function flush(): Promise<void>;
declare function disable(): void; declare function disable(): void;
declare function enable(): void; declare function enable(): void;
export { init, getClient, captureException, captureMessage, captureEvent, setUser, setTag, setTags, setExtra, addBreadcrumb, flush, disable, enable, Client, ErrorPlugin, PerformancePlugin, NetworkPlugin, BehaviorPlugin, OfflinePlugin, SamplingPlugin, }; export { init, getClient, captureException, captureMessage, captureEvent, setUser, setAnonymousId, clearAnonymousId, getAnonymousId, startTransaction, getCurrentTransaction, getCurrentSpan, setSpan, startSpan, setTag, setTags, setExtra, addBreadcrumb, flush, disable, enable, Client, httpStatusToSpanStatus, dbStatusToSpanStatus, statusToSpanStatus, serializeBaggage, parseBaggage, createBaggageFromTransaction, ErrorPlugin, PerformancePlugin, NetworkPlugin, BehaviorPlugin, OfflinePlugin, SamplingPlugin, RouterPlugin, InteractionPlugin, };
export type { LightConfig, LightClient, LightPlugin, SentryEvent, ErrorEvent, PerformanceEvent, NetworkEvent, BehaviorEvent, EventLevel, UserInfo, Breadcrumb, DSNInfo, }; export { SpanClass as Span, TransactionClass as Transaction };
export type { LightConfig, LightClient, LightPlugin, SentryEvent, ErrorEvent, PerformanceEvent, NetworkEvent, BehaviorEvent, EventLevel, UserInfo, Breadcrumb, DSNInfo, TransactionContext, SpanContext, SamplingContext, BaggageFields, SpanStatus, SpanJSON, TransactionJSON, };
declare const _default: { declare const _default: {
init: typeof init; init: typeof init;
getClient: typeof getClient; getClient: typeof getClient;
@ -30,6 +43,14 @@ declare const _default: {
captureMessage: typeof captureMessage; captureMessage: typeof captureMessage;
captureEvent: typeof captureEvent; captureEvent: typeof captureEvent;
setUser: typeof setUser; setUser: typeof setUser;
setAnonymousId: typeof setAnonymousId;
clearAnonymousId: typeof clearAnonymousId;
getAnonymousId: typeof getAnonymousId;
startTransaction: typeof startTransaction;
getCurrentTransaction: typeof getCurrentTransaction;
getCurrentSpan: typeof getCurrentSpan;
setSpan: typeof setSpan;
startSpan: typeof startSpan;
setTag: typeof setTag; setTag: typeof setTag;
setTags: typeof setTags; setTags: typeof setTags;
setExtra: typeof setExtra; setExtra: typeof setExtra;
@ -37,5 +58,8 @@ declare const _default: {
flush: typeof flush; flush: typeof flush;
disable: typeof disable; disable: typeof disable;
enable: typeof enable; enable: typeof enable;
Span: typeof SpanClass;
Transaction: typeof TransactionClass;
httpStatusToSpanStatus: typeof httpStatusToSpanStatus;
}; };
export default _default; export default _default;

6
dist/index.esm.js vendored

File diff suppressed because one or more lines are too long

6
dist/index.iife.js vendored

File diff suppressed because one or more lines are too long

24
dist/plugins/InteractionPlugin.d.ts vendored Normal file
View File

@ -0,0 +1,24 @@
import type { LightPlugin, LightClient } from '../types';
declare class InteractionPlugin implements LightPlugin {
name: string;
version: string;
private client;
private config;
private currentTransaction;
private idleTimer;
private maxDurationTimer;
private debounceTimer;
setup(client: LightClient): void;
private loadConfig;
private handleClick;
private handleSubmit;
private isEventIgnored;
private scheduleInteractionTransaction;
private getElementName;
private startInteractionTransaction;
private scheduleIdleFinish;
private scheduleMaxDuration;
private finishCurrentTransaction;
destroy(): void;
}
export default InteractionPlugin;

View File

@ -10,7 +10,12 @@ declare class NetworkPlugin implements LightPlugin {
setup(client: LightClient): void; setup(client: LightClient): void;
private loadConfig; private loadConfig;
private shouldIgnore; private shouldIgnore;
private shouldTracePropagate;
private isSameOrigin;
private getBaggageHeader;
private shouldSample; private shouldSample;
private createHttpSpan;
private finishHttpSpan;
private patchFetch; private patchFetch;
private patchXHR; private patchXHR;
private reportNetwork; private reportNetwork;

View File

@ -12,22 +12,30 @@ declare class PerformancePlugin implements LightPlugin {
private lastLongTaskEndTime; private lastLongTaskEndTime;
private ttiReported; private ttiReported;
private ttiTimer; private ttiTimer;
private pageLoadTransaction;
private measurements;
private resourceSpanCount;
setup(client: LightClient): void; setup(client: LightClient): void;
private startPageLoadTransaction;
private loadConfig; private loadConfig;
private shouldSample; private shouldSample;
private observeWebVitals; private observeWebVitals;
private observeFP; private observePaint;
private observeLCP; private observeLCP;
private observeFID; private observeFID;
private observeCLS; private observeCLS;
private observeFCP;
private observeTTFB; private observeTTFB;
private observeLongTasks; private observeLongTasks;
private scheduleTTI; private scheduleTTI;
private observeNavigation; private observeNavigation;
private buildNavigationSpans;
private finishPageLoadTransaction;
private observeResources; private observeResources;
private getRating; private getRating;
private reportMetric; private reportMetric;
private recordMeasurement;
private createNavigationSpan;
private createResourceSpan;
destroy(): void; destroy(): void;
} }
export default PerformancePlugin; export default PerformancePlugin;

26
dist/plugins/RouterPlugin.d.ts vendored Normal file
View File

@ -0,0 +1,26 @@
import type { LightPlugin, LightClient } from '../types';
declare class RouterPlugin implements LightPlugin {
name: string;
version: string;
private client;
private config;
private currentTransaction;
private lastRoute;
private originalPushState?;
private originalReplaceState?;
private maxDurationTimer;
private routeChangeTimer;
setup(client: LightClient): void;
private loadConfig;
private instrumentHistory;
private handlePopState;
private instrumentHash;
private handleHashChange;
private handleRouteChange;
private processRouteChange;
private startNavigationTransaction;
private startMaxDurationTimer;
private finishCurrentTransaction;
destroy(): void;
}
export default RouterPlugin;

122
dist/types/index.d.ts vendored
View File

@ -44,10 +44,31 @@ export interface DeviceContext {
brand?: string; brand?: string;
type?: string; type?: string;
} }
export interface TraceContext {
trace_id: string;
span_id: string;
parent_span_id?: string;
op?: string;
status?: string;
description?: string;
}
export interface BaggageFields {
trace_id?: string;
public_key?: string;
sample_rate?: string;
sampled?: string;
user_id?: string;
user_segment?: string;
release?: string;
environment?: string;
replay_id?: string;
[key: string]: string | undefined;
}
export interface Contexts { export interface Contexts {
browser?: BrowserContext; browser?: BrowserContext;
os?: OSContext; os?: OSContext;
device?: DeviceContext; device?: DeviceContext;
trace?: TraceContext;
} }
export interface Breadcrumb { export interface Breadcrumb {
type: string; type: string;
@ -64,6 +85,7 @@ export interface BaseEvent {
release?: string; release?: string;
environment?: string; environment?: string;
user?: UserInfo; user?: UserInfo;
anonymousId?: string;
tags?: Record<string, string>; tags?: Record<string, string>;
extra?: Record<string, unknown>; extra?: Record<string, unknown>;
breadcrumbs?: Breadcrumb[]; breadcrumbs?: Breadcrumb[];
@ -111,7 +133,91 @@ export interface CountEvent extends BaseEvent {
ts_start: number; ts_start: number;
ts_end: number; ts_end: number;
} }
export type SentryEvent = ErrorEvent | PerformanceEvent | NetworkEvent | BehaviorEvent | CountEvent; export type SpanStatus = 'ok' | 'cancelled' | 'unknown' | 'unknown_error' | 'invalid_argument' | 'deadline_exceeded' | 'not_found' | 'already_exists' | 'permission_denied' | 'resource_exhausted' | 'failed_precondition' | 'aborted' | 'out_of_range' | 'unimplemented' | 'internal_error' | 'unavailable' | 'data_loss' | 'unauthenticated';
export interface SpanContext {
op?: string;
description?: string;
startTimestamp?: number;
status?: SpanStatus;
tags?: Record<string, string>;
data?: Record<string, unknown>;
traceId?: string;
spanId?: string;
parentSpanId?: string;
sampled?: boolean;
}
export interface SamplingContext extends TransactionContext {
parentSampled?: boolean;
}
export interface TransactionContext extends SpanContext {
name: string;
type?: string;
}
export interface SpanJSON {
trace_id: string;
span_id: string;
parent_span_id?: string;
op?: string;
description?: string;
status?: SpanStatus;
tags?: Record<string, string>;
data?: Record<string, unknown>;
start_timestamp: number;
timestamp: number;
}
export interface TransactionJSON extends SpanJSON {
transaction: string;
type?: string;
spans: SpanJSON[];
sampled?: boolean;
}
export interface Span {
readonly traceId: string;
readonly spanId: string;
readonly parentSpanId?: string;
op?: string;
description?: string;
status?: SpanStatus;
tags: Record<string, string>;
data: Record<string, unknown>;
readonly startTimestamp: number;
readonly endTimestamp?: number;
readonly transaction?: Transaction;
setTag(key: string, value: string): this;
setData(key: string, value: unknown): this;
setStatus(status: SpanStatus): this;
setHttpStatus(statusCode: number): this;
setStatusFromResponse(status: string | number | undefined, tagKey?: string): this;
startChild(context?: SpanContext): Span;
finish(endTimestamp?: number): void;
toTraceparent(): string;
toJSON(): SpanJSON;
}
export interface Transaction extends Omit<Span, 'toJSON'> {
name: string;
type?: string;
spans: Span[];
sampled?: boolean;
maxSpans?: number;
toJSON(): TransactionJSON;
toEvent(): TransactionEvent;
}
export interface MeasurementValue {
value: number;
unit?: string;
rating?: 'good' | 'needs-improvement' | 'poor';
}
export interface TransactionEvent extends BaseEvent {
type: 'transaction';
transaction: string;
transaction_info?: {
source: string;
};
spans: Array<ReturnType<Span['toJSON']>>;
start_timestamp: number;
measurements?: Record<string, MeasurementValue>;
}
export type SentryEvent = ErrorEvent | PerformanceEvent | NetworkEvent | BehaviorEvent | CountEvent | TransactionEvent;
export interface DSNInfo { export interface DSNInfo {
protocol: string; protocol: string;
publicKey: string; publicKey: string;
@ -134,6 +240,13 @@ export interface LightConfig {
enabled?: boolean; enabled?: boolean;
debug?: boolean; debug?: boolean;
sampleRate?: number; sampleRate?: number;
tracesSampleRate?: number;
tracesSampler?: (context: SamplingContext) => number | boolean;
/**
* @deprecated 使 tracePropagationTargets
*/
tracingOrigins?: (string | RegExp)[];
tracePropagationTargets?: (string | RegExp)[];
maxQueueSize?: number; maxQueueSize?: number;
flushInterval?: number; flushInterval?: number;
maxRetries?: number; maxRetries?: number;
@ -144,6 +257,7 @@ export interface LightConfig {
maxBreadcrumbs?: number; maxBreadcrumbs?: number;
beforeSend?: (event: SentryEvent) => SentryEvent | null; beforeSend?: (event: SentryEvent) => SentryEvent | null;
user?: UserInfo; user?: UserInfo;
trackAnonymousUsers?: boolean;
plugins?: (LightPlugin | string)[]; plugins?: (LightPlugin | string)[];
contextLevel?: Partial<ContextLevelMap>; contextLevel?: Partial<ContextLevelMap>;
sampling?: SamplingConfig; sampling?: SamplingConfig;
@ -169,10 +283,16 @@ export interface LightClient {
type: string; type: string;
}): void; }): void;
setUser(user: UserInfo | null): void; setUser(user: UserInfo | null): void;
setAnonymousId(id: string): void;
setTag(key: string, value: string): void; setTag(key: string, value: string): void;
setTags(tags: Record<string, string>): void; setTags(tags: Record<string, string>): void;
setExtra(key: string, value: unknown): void; setExtra(key: string, value: unknown): void;
addBreadcrumb(breadcrumb: Omit<Breadcrumb, 'timestamp'>): void; addBreadcrumb(breadcrumb: Omit<Breadcrumb, 'timestamp'>): void;
startTransaction(context: TransactionContext): Transaction | null;
getCurrentTransaction(): Transaction | null;
getCurrentSpan(): Span | null;
setSpan(span: Span | null): void;
startSpan(context: SpanContext): Span | null;
flush(): Promise<void>; flush(): Promise<void>;
disable(): void; disable(): void;
enable(): void; enable(): void;

14
dist/utils/baggage.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
import type { BaggageFields } from '../types';
export declare function serializeBaggage(fields: BaggageFields): string;
export declare function parseBaggage(header: string): BaggageFields;
export declare function createBaggageFromTransaction(traceId: string, options?: {
dsnPublicKey?: string;
sampled?: boolean;
sampleRate?: number;
user?: {
id?: string;
segment?: string;
};
release?: string;
environment?: string;
}): BaggageFields;

21
dist/utils/identity.d.ts vendored Normal file
View File

@ -0,0 +1,21 @@
/**
* ID
* localStorage ID
*/
export declare function getAnonymousId(): string | null;
/**
* ID
* ID
*/
export declare function setAnonymousId(id: string): boolean;
/**
* ID
*
*/
export declare function clearAnonymousId(): boolean;
/**
*
*/
export declare function isAnonymousTrackingEnabled(config: {
trackAnonymousUsers?: boolean;
}): boolean;

148
dist/utils/tracing.d.ts vendored Normal file
View File

@ -0,0 +1,148 @@
import type { Span as SpanInterface, SpanContext, Transaction as TransactionInterface, TransactionContext, SpanStatus, LightClient, BaggageFields } from '../types';
declare function generateTraceId(): string;
declare function generateSpanId(): string;
export declare function httpStatusToSpanStatus(statusCode: number): SpanStatus;
export declare function dbStatusToSpanStatus(dbStatusCode?: string | number): SpanStatus;
export declare function statusToSpanStatus(status: string | number | undefined, op: string | undefined): SpanStatus;
export declare class Span implements SpanInterface {
readonly traceId: string;
readonly spanId: string;
readonly parentSpanId?: string;
op?: string;
description?: string;
status?: SpanStatus;
tags: Record<string, string>;
data: Record<string, unknown>;
readonly startTimestamp: number;
endTimestamp?: number;
transaction?: TransactionInterface;
protected _isFinished: boolean;
protected _client?: LightClient;
constructor(context: SpanContext, transaction?: TransactionInterface, client?: LightClient);
setTag(key: string, value: string): this;
setData(key: string, value: unknown): this;
setStatus(status: SpanStatus): this;
setHttpStatus(statusCode: number): this;
setStatusFromResponse(status: string | number | undefined, tagKey?: string): this;
startChild(context?: SpanContext): Span;
finish(endTimestamp?: number): void;
toTraceparent(): string;
toJSON(): {
trace_id: string;
span_id: string;
parent_span_id: string;
op: string;
description: string;
status: SpanStatus;
tags: {
[x: string]: string;
};
data: {
[x: string]: unknown;
};
start_timestamp: number;
timestamp: number;
};
}
export declare class Transaction extends Span implements TransactionInterface {
name: string;
type?: string;
spans: Span[];
sampled?: boolean;
maxSpans: number;
private _baggage?;
private _dsnPublicKey?;
private _release?;
private _environment?;
private _droppedSpans;
constructor(context: TransactionContext, client?: LightClient, dsnPublicKey?: string, release?: string, environment?: string, maxSpans?: number);
_addSpan(span: Span): void;
private _ensureBaggage;
setBaggage(key: string, value: string): this;
setUserBaggage(userId?: string, segment?: string): this;
toBaggage(): string | undefined;
getBaggage(): BaggageFields | undefined;
finish(endTimestamp?: number): void;
private _getSource;
toEvent(): {
type: "transaction";
transaction: string;
transaction_info: {
source: string;
};
level: "info";
timestamp: number;
start_timestamp: number;
spans: {
trace_id: string;
span_id: string;
parent_span_id: string;
op: string;
description: string;
status: SpanStatus;
tags: {
[x: string]: string;
};
data: {
[x: string]: unknown;
};
start_timestamp: number;
timestamp: number;
}[];
contexts: {
trace: {
trace_id: string;
span_id: string;
parent_span_id: string;
op: string;
status: SpanStatus;
};
};
tags: {
[x: string]: string;
};
extra: Record<string, unknown>;
measurements: Record<string, {
value: number;
unit?: string;
rating?: "good" | "needs-improvement" | "poor";
}>;
platform: string;
};
toJSON(): {
transaction: string;
type: string;
spans: {
trace_id: string;
span_id: string;
parent_span_id: string;
op: string;
description: string;
status: SpanStatus;
tags: {
[x: string]: string;
};
data: {
[x: string]: unknown;
};
start_timestamp: number;
timestamp: number;
}[];
sampled: boolean;
trace_id: string;
span_id: string;
parent_span_id: string;
op: string;
description: string;
status: SpanStatus;
tags: {
[x: string]: string;
};
data: {
[x: string]: unknown;
};
start_timestamp: number;
timestamp: number;
};
}
export { generateTraceId, generateSpanId };

348
docs/07-tracing.md Normal file
View File

@ -0,0 +1,348 @@
# 分布式追踪Distributed Tracing设计文档
## 一、背景与目标
### 1.1 背景
当前 SDK 已支持错误采集、性能指标Web Vitals、网络请求监控、用户行为采集等能力但缺乏**前后端链路打通**的分布式追踪能力。当用户遇到"接口慢"或"报错"时,无法关联前端操作与后端服务的内部调用链路,排查问题效率低。
### 1.2 目标
- **第一阶段P0**:实现 Trace/Span 核心模型 + 前后端 Trace 传播 + 手动 API
- **第二阶段P1**:自动 InstrumentationPage Load、路由变化、用户交互
- **第三阶段P2**:采样策略升级 + Baggage 支持 + 完整开发者体验
---
## 二、核心概念
### 2.1 Trace
一条完整的分布式调用链路,全局唯一 `trace_id` 标识。一条 Trace 跨越多个服务、多个进程。
### 2.2 Transaction
Trace 中的"根 Span",代表一个完整的用户操作或业务流程。前端常见的 Transaction 类型:
- `pageload`:页面加载
- `navigation`路由切换SPA
- `user-interaction`:用户交互(点击、提交等)
- `custom`:自定义事务
### 2.3 Span
Transaction 中的具体操作步骤,有父子关系。常见 Span 类型:
- `http`HTTP 请求
- `resource`:资源加载
- `browser`浏览器内部操作DOM 解析、脚本执行等)
- `db`:数据库查询(后端)
- `middleware`:中间件(后端)
### 2.4 Span 关系
```
Trace (trace_id = abc123)
└── Transaction: "page-load" (span_id = s1)
├── Span: "http GET /api/user" (span_id = s2, parent = s1)
│ └── 后端服务的多个 Span通过 trace header 关联)
├── Span: "resource /static/app.js" (span_id = s3, parent = s1)
└── Span: "dom-parse" (span_id = s4, parent = s1)
```
---
## 三、数据模型设计
### 3.1 Span 接口
```typescript
interface Span {
trace_id: string; // 16/32 位十六进制,全局唯一
span_id: string; // 16 位十六进制Span 唯一
parent_span_id?: string; // 父 Span ID
op?: string; // 操作类型http、resource、browser 等
description?: string; // 描述信息
start_timestamp: number; // 开始时间(毫秒时间戳)
timestamp: number; // 结束时间(毫秒时间戳)
status?: SpanStatus; // 状态
tags?: Record<string, string>; // 标签
data?: Record<string, unknown>; // 附加数据
same_process_as_parent?: boolean;
}
type SpanStatus =
| 'ok'
| 'cancelled'
| 'unknown'
| 'invalid_argument'
| 'deadline_exceeded'
| 'not_found'
| 'already_exists'
| 'permission_denied'
| 'resource_exhausted'
| 'failed_precondition'
| 'aborted'
| 'out_of_range'
| 'unimplemented'
| 'internal_error'
| 'unavailable'
| 'data_loss'
| 'unauthenticated';
```
### 3.2 Transaction 接口
```typescript
interface Transaction extends Span {
name: string; // 事务名称(如 "GET /api/users"
spans: Span[]; // 子 Span 列表
transaction: string; // 事务名称冗余字段
type?: string; // 事务类型page-load、navigation 等
sampled?: boolean; // 是否采样
}
```
### 3.3 上报事件类型
新增 `transaction` 事件类型:
```typescript
interface TransactionEvent extends BaseEvent {
type: 'transaction';
transaction: string;
spans: Span[];
start_timestamp: number;
timestamp: number;
// ...
}
```
---
## 四、前后端 Trace 传播
### 4.1 协议选择
采用 **Sentry Trace 协议**`sentry-trace` header同时支持 W3C Trace Context 作为兼容。
### 4.2 sentry-trace Header 格式
```
sentry-trace: {trace_id}-{span_id}-{sampled}
```
- `trace_id`: 32 位十六进制字符串
- `span_id`: 16 位十六进制字符串
- `sampled`: 可选,`1` = 采样,`0` = 不采样
示例:
```
sentry-trace: 8839b8a3914c485bb1f52ce3c08c9c0b-b89e261f348c022b-1
```
### 4.3 baggage Header后续阶段
```
baggage: sentry-trace_id=xxx,sentry-public_key=xxx,sentry-sample_rate=1.0
```
### 4.4 追踪域名白名单
为避免向第三方接口泄漏 trace 信息,需要配置 `tracingOrigins`
```typescript
LightSentry.init({
dsn: '...',
tracingOrigins: [
'api.example.com',
/^https:\/\/internal\./,
'localhost',
],
});
```
默认匹配同源域名。
---
## 五、核心模块设计
### 5.1 TracingManager
职责:
- 维护当前活跃的 Transaction
- 维护 Span 栈(当前执行上下文)
- 生成 trace_id / span_id
- 管理 Span 的父子关系
关键方法:
```typescript
class TracingManager {
startTransaction(context: TransactionContext): Transaction;
getCurrentTransaction(): Transaction | null;
setCurrentTransaction(transaction: Transaction | null): void;
startSpan(context: SpanContext): Span | null;
getCurrentSpan(): Span | null;
finishSpan(span: Span): void;
}
```
### 5.2 Span 类
```typescript
class Span {
traceId: string;
spanId: string;
parentSpanId?: string;
op?: string;
description?: string;
startTimestamp: number;
endTimestamp?: number;
status?: string;
tags: Record<string, string>;
data: Record<string, unknown>;
setTag(key: string, value: string): this;
setData(key: string, value: unknown): this;
setStatus(status: string): this;
startChild(context: SpanContext): Span;
finish(endTimestamp?: number): void;
toJSON(): Span;
toTraceparent(): string; // 生成 sentry-trace header 值
}
```
### 5.3 Transaction 类
```typescript
class Transaction extends Span {
name: string;
spans: Span[];
type?: string;
sampled?: boolean;
finish(endTimestamp?: number): void;
toJSON(): Transaction;
}
```
---
## 六、插件改造
### 6.1 NetworkPlugin 改造
**改造前**:上报独立的 `network` 事件
**改造后**:创建 `http` Span关联到当前 Transaction并注入 trace header
具体改动:
1. 请求开始时,如果存在活跃 Transaction创建 Span
2. 向匹配的域名注入 `sentry-trace` header
3. 请求结束时,设置 Span status 和 datafinish Span
4. 保持原有 `network` 事件上报(兼容)
### 6.2 PerformancePlugin 改造(第二阶段)
改造为 Page Load Transaction
- Navigation Timing → 拆解为多个 Span
- Resource Timing → 作为 resource Span
- Web Vitals → 作为标记点或 measurement
### 6.3 新增 RouterPlugin第二阶段
监听路由变化:
- History APIpushState / replaceState / popstate
- hashchange
- 每次路由变化创建新的 navigation Transaction
---
## 七、对外 API
### 7.1 顶层 API
```typescript
// 开始事务
LightSentry.startTransaction({
name: 'search-products',
op: 'user-interaction',
});
// 获取当前 Span
const span = LightSentry.getCurrentSpan();
// 创建子 Span基于当前活跃 Span
const childSpan = LightSentry.startSpan({
op: 'http',
description: 'GET /api/search',
});
// 手动设置 Span
LightSentry.setSpan(span);
```
### 7.2 配置项
```typescript
interface LightConfig {
// ...
tracesSampleRate?: number; // 全局 Trace 采样率 0~1默认 0
tracesSampler?: (context: any) => number; // 动态采样函数
tracingOrigins?: (string | RegExp)[]; // 追踪域名白名单
tracePropagationTargets?: (string | RegExp)[]; // trace header 传播目标
}
```
---
## 八、采样策略(第一阶段简化版)
第一阶段采用简单策略:
1. 配置 `tracesSampleRate`(默认 0 = 关闭)
2. Transaction 创建时决定是否采样
3. 采样决策通过 `sentry-trace``sampled` 标志传递给后端
4. 未采样的 Transaction 不上报,但仍传递 trace_id用于关联
---
## 九、实施计划
### 第一阶段:核心能力(已完成)
- [x] 设计文档
- [x] 新增 Trace/Span 类型定义
- [x] 实现 Span 类
- [x] 实现 Transaction 类
- [x] 实现 TracingManager
- [x] 改造 NetworkPlugin注入 trace header + 创建 HTTP Span
- [x] 对外暴露 startTransaction / startSpan API
- [x] 集成到 Client
- [x] 编译验证
- [x] 错误事件自动关联 Trace 上下文
- [x] Transaction 完成后自动上报
- [x] Span 栈生命周期管理
- [x] 未采样 Transaction 不收集 Span性能优化
### 第二阶段:自动 Instrumentation已完成
- [x] PerformancePlugin 改造Page Load Transaction
- [x] Navigation Timing 拆解为 browser.* Span 树
- [x] Resource Timing 转为 resource.* Span
- [x] Web Vitals 作为 measurements 附加到 Transaction
- [x] LCP/FP/FCP 等关键指标标记
- [x] 新增 RouterPlugin路由变化追踪
- [x] History API 监听pushState / replaceState / popstate
- [x] Hash 路由监听hashchange
- [x] 路由切换期间的 Span 自动关联
- [x] 新增 InteractionPlugin用户交互追踪
- [x] 点击/表单提交等交互自动创建 Transaction
- [x] 交互期间的网络请求自动关联
- [x] Error 与 Trace 深度集成
- [x] 错误事件自动附加 contexts.trace
- [ ] 错误作为 error Span 加入当前 Transaction可选
### 第三阶段:进阶能力(已完成)
- [x] Baggage header 支持
- [x] Transaction 携带 user、release 等信息
- [x] NetworkPlugin 自动注入 baggage header
- [x] 后端可以读取 baggage 传递的信息
- [x] 动态采样 tracesSampler 实现
- [x] tracesSampler 函数支持
- [x] 根据 transaction 上下文动态决定采样率
- [x] 完整 Span Status 映射增强
- [x] 数据库操作状态映射dbStatusToSpanStatus
- [x] 自定义操作状态映射statusToSpanStatus
- [x] Span status 与 error 关联setStatusFromResponse
- [x] 错误作为 error Span 加入 Transaction可选功能
---
## 十、兼容性考虑
1. **向后兼容**:默认 `tracesSampleRate = 0`,即默认关闭,不影响现有功能
2. **事件类型新增**:新增 `transaction` 事件类型,后端需支持
3. **NetworkPlugin**:保持原有 `network` 事件上报,同时新增 Span可配置开关
4. **浏览器兼容性**:不依赖新 API使用现有能力实现
---
## 十一、参考资料
- [Sentry Tracing 文档](https://docs.sentry.io/product/sentry-basics/concepts/tracing/)
- [Sentry Performance 数据模型](https://develop.sentry.dev/sdk/performance/)
- [W3C Trace Context](https://www.w3.org/TR/trace-context/)

461
package-lock.json generated
View File

@ -9,12 +9,27 @@
"version": "0.1.0", "version": "0.1.0",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@vitest/coverage-v8": "^1.6.0",
"esbuild": "^0.20.0", "esbuild": "^0.20.0",
"jsdom": "^24.0.0", "jsdom": "^24.0.0",
"typescript": "^5.4.0", "typescript": "^5.4.0",
"vitest": "^1.4.0" "vitest": "^1.4.0"
} }
}, },
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@asamuzakjp/css-color": { "node_modules/@asamuzakjp/css-color": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", "resolved": "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
@ -29,6 +44,63 @@
"lru-cache": "^10.4.3" "lru-cache": "^10.4.3"
} }
}, },
"node_modules/@babel/helper-string-parser": {
"version": "7.29.7",
"resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.29.7",
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.29.7",
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz",
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.29.7"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/types": {
"version": "7.29.7",
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz",
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.29.7",
"@babel/helper-validator-identifier": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@bcoe/v8-coverage": {
"version": "0.2.3",
"resolved": "https://registry.npmmirror.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true,
"license": "MIT"
},
"node_modules/@csstools/color-helpers": { "node_modules/@csstools/color-helpers": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", "resolved": "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
@ -535,6 +607,16 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/@istanbuljs/schema": {
"version": "0.1.6",
"resolved": "https://registry.npmmirror.com/@istanbuljs/schema/-/schema-0.1.6.tgz",
"integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/@jest/schemas": { "node_modules/@jest/schemas": {
"version": "29.6.3", "version": "29.6.3",
"resolved": "https://registry.npmmirror.com/@jest/schemas/-/schemas-29.6.3.tgz", "resolved": "https://registry.npmmirror.com/@jest/schemas/-/schemas-29.6.3.tgz",
@ -548,6 +630,27 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0" "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
} }
}, },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": { "node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5", "version": "1.5.5",
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
@ -555,6 +658,17 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.62.2", "version": "4.62.2",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
@ -919,15 +1033,43 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vitest/expect": { "node_modules/@vitest/coverage-v8": {
"version": "1.6.1", "version": "1.6.0",
"resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-1.6.1.tgz", "resolved": "https://registry.npmmirror.com/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz",
"integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", "integrity": "sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/spy": "1.6.1", "@ampproject/remapping": "^2.2.1",
"@vitest/utils": "1.6.1", "@bcoe/v8-coverage": "^0.2.3",
"debug": "^4.3.4",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
"istanbul-lib-source-maps": "^5.0.4",
"istanbul-reports": "^3.1.6",
"magic-string": "^0.30.5",
"magicast": "^0.3.3",
"picocolors": "^1.0.0",
"std-env": "^3.5.0",
"strip-literal": "^2.0.0",
"test-exclude": "^6.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"vitest": "1.6.0"
}
},
"node_modules/@vitest/expect": {
"version": "1.6.0",
"resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-1.6.0.tgz",
"integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "1.6.0",
"@vitest/utils": "1.6.0",
"chai": "^4.3.10" "chai": "^4.3.10"
}, },
"funding": { "funding": {
@ -935,13 +1077,13 @@
} }
}, },
"node_modules/@vitest/runner": { "node_modules/@vitest/runner": {
"version": "1.6.1", "version": "1.6.0",
"resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-1.6.1.tgz", "resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-1.6.0.tgz",
"integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/utils": "1.6.1", "@vitest/utils": "1.6.0",
"p-limit": "^5.0.0", "p-limit": "^5.0.0",
"pathe": "^1.1.1" "pathe": "^1.1.1"
}, },
@ -950,9 +1092,9 @@
} }
}, },
"node_modules/@vitest/snapshot": { "node_modules/@vitest/snapshot": {
"version": "1.6.1", "version": "1.6.0",
"resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-1.6.1.tgz", "resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-1.6.0.tgz",
"integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -965,9 +1107,9 @@
} }
}, },
"node_modules/@vitest/spy": { "node_modules/@vitest/spy": {
"version": "1.6.1", "version": "1.6.0",
"resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-1.6.1.tgz", "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-1.6.0.tgz",
"integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -978,9 +1120,9 @@
} }
}, },
"node_modules/@vitest/utils": { "node_modules/@vitest/utils": {
"version": "1.6.1", "version": "1.6.0",
"resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-1.6.1.tgz", "resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-1.6.0.tgz",
"integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1059,6 +1201,24 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "1.1.15",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.15.tgz",
"integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/cac": { "node_modules/cac": {
"version": "6.7.14", "version": "6.7.14",
"resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz", "resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz",
@ -1128,6 +1288,13 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT"
},
"node_modules/confbox": { "node_modules/confbox": {
"version": "0.1.8", "version": "0.1.8",
"resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz", "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz",
@ -1410,6 +1577,13 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true,
"license": "ISC"
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
@ -1497,6 +1671,28 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
@ -1510,6 +1706,16 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/has-symbols": { "node_modules/has-symbols": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
@ -1565,6 +1771,13 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/html-escaper/-/html-escaper-2.0.2.tgz",
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true,
"license": "MIT"
},
"node_modules/http-proxy-agent": { "node_modules/http-proxy-agent": {
"version": "7.0.2", "version": "7.0.2",
"resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
@ -1616,6 +1829,25 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dev": true,
"license": "ISC",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true,
"license": "ISC"
},
"node_modules/is-potential-custom-element-name": { "node_modules/is-potential-custom-element-name": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmmirror.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
@ -1643,6 +1875,60 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/istanbul-lib-coverage": {
"version": "3.2.2",
"resolved": "https://registry.npmmirror.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=8"
}
},
"node_modules/istanbul-lib-report": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"istanbul-lib-coverage": "^3.0.0",
"make-dir": "^4.0.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/istanbul-lib-source-maps": {
"version": "5.0.6",
"resolved": "https://registry.npmmirror.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
"integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.23",
"debug": "^4.1.1",
"istanbul-lib-coverage": "^3.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/istanbul-reports": {
"version": "3.2.0",
"resolved": "https://registry.npmmirror.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"html-escaper": "^2.0.0",
"istanbul-lib-report": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "9.0.1", "version": "9.0.1",
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz", "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz",
@ -1735,6 +2021,34 @@
"@jridgewell/sourcemap-codec": "^1.5.5" "@jridgewell/sourcemap-codec": "^1.5.5"
} }
}, },
"node_modules/magicast": {
"version": "0.3.5",
"resolved": "https://registry.npmmirror.com/magicast/-/magicast-0.3.5.tgz",
"integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.25.4",
"@babel/types": "^7.25.4",
"source-map-js": "^1.2.0"
}
},
"node_modules/make-dir": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-4.0.0.tgz",
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.5.3"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -1788,6 +2102,19 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/mlly": { "node_modules/mlly": {
"version": "1.8.2", "version": "1.8.2",
"resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.2.tgz", "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.2.tgz",
@ -1870,6 +2197,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/onetime": { "node_modules/onetime": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmmirror.com/onetime/-/onetime-6.0.0.tgz", "resolved": "https://registry.npmmirror.com/onetime/-/onetime-6.0.0.tgz",
@ -1915,6 +2252,16 @@
"url": "https://github.com/inikulin/parse5?sponsor=1" "url": "https://github.com/inikulin/parse5?sponsor=1"
} }
}, },
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-key": { "node_modules/path-key": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
@ -2128,6 +2475,19 @@
"node": ">=v12.22.7" "node": ">=v12.22.7"
} }
}, },
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
@ -2221,6 +2581,19 @@
"url": "https://github.com/sponsors/antfu" "url": "https://github.com/sponsors/antfu"
} }
}, },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/symbol-tree": { "node_modules/symbol-tree": {
"version": "3.2.4", "version": "3.2.4",
"resolved": "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz", "resolved": "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz",
@ -2228,6 +2601,21 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/test-exclude": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/test-exclude/-/test-exclude-6.0.0.tgz",
"integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
"dev": true,
"license": "ISC",
"dependencies": {
"@istanbuljs/schema": "^0.1.2",
"glob": "^7.1.4",
"minimatch": "^3.0.4"
},
"engines": {
"node": ">=8"
}
},
"node_modules/tinybench": { "node_modules/tinybench": {
"version": "2.9.0", "version": "2.9.0",
"resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz", "resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz",
@ -2397,9 +2785,9 @@
} }
}, },
"node_modules/vite-node": { "node_modules/vite-node": {
"version": "1.6.1", "version": "1.6.0",
"resolved": "https://registry.npmmirror.com/vite-node/-/vite-node-1.6.1.tgz", "resolved": "https://registry.npmmirror.com/vite-node/-/vite-node-1.6.0.tgz",
"integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -2850,17 +3238,17 @@
} }
}, },
"node_modules/vitest": { "node_modules/vitest": {
"version": "1.6.1", "version": "1.6.0",
"resolved": "https://registry.npmmirror.com/vitest/-/vitest-1.6.1.tgz", "resolved": "https://registry.npmmirror.com/vitest/-/vitest-1.6.0.tgz",
"integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/expect": "1.6.1", "@vitest/expect": "1.6.0",
"@vitest/runner": "1.6.1", "@vitest/runner": "1.6.0",
"@vitest/snapshot": "1.6.1", "@vitest/snapshot": "1.6.0",
"@vitest/spy": "1.6.1", "@vitest/spy": "1.6.0",
"@vitest/utils": "1.6.1", "@vitest/utils": "1.6.0",
"acorn-walk": "^8.3.2", "acorn-walk": "^8.3.2",
"chai": "^4.3.10", "chai": "^4.3.10",
"debug": "^4.3.4", "debug": "^4.3.4",
@ -2874,7 +3262,7 @@
"tinybench": "^2.5.1", "tinybench": "^2.5.1",
"tinypool": "^0.8.3", "tinypool": "^0.8.3",
"vite": "^5.0.0", "vite": "^5.0.0",
"vite-node": "1.6.1", "vite-node": "1.6.0",
"why-is-node-running": "^2.2.2" "why-is-node-running": "^2.2.2"
}, },
"bin": { "bin": {
@ -2889,8 +3277,8 @@
"peerDependencies": { "peerDependencies": {
"@edge-runtime/vm": "*", "@edge-runtime/vm": "*",
"@types/node": "^18.0.0 || >=20.0.0", "@types/node": "^18.0.0 || >=20.0.0",
"@vitest/browser": "1.6.1", "@vitest/browser": "1.6.0",
"@vitest/ui": "1.6.1", "@vitest/ui": "1.6.0",
"happy-dom": "*", "happy-dom": "*",
"jsdom": "*" "jsdom": "*"
}, },
@ -3009,6 +3397,13 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true,
"license": "ISC"
},
"node_modules/ws": { "node_modules/ws": {
"version": "8.21.0", "version": "8.21.0",
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.21.0.tgz", "resolved": "https://registry.npmmirror.com/ws/-/ws-8.21.0.tgz",

View File

@ -26,6 +26,7 @@
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@vitest/coverage-v8": "^1.6.0",
"esbuild": "^0.20.0", "esbuild": "^0.20.0",
"jsdom": "^24.0.0", "jsdom": "^24.0.0",
"typescript": "^5.4.0", "typescript": "^5.4.0",

View File

@ -1,14 +1,16 @@
import type { LightConfig, LightClient, LightPlugin, DSNInfo, SentryEvent, EventLevel, UserInfo, Breadcrumb, ErrorEvent } from '../types'; import type { LightConfig, LightClient, LightPlugin, DSNInfo, SentryEvent, EventLevel, UserInfo, Breadcrumb, ErrorEvent, TransactionContext, SpanContext, Transaction, Span } from '../types';
import { parseDSN } from '../utils/dsn'; import { parseDSN } from '../utils/dsn';
import { now } from '../utils/helper'; import { now } from '../utils/helper';
import { computeFingerprint } from '../utils/hash'; import { computeFingerprint } from '../utils/hash';
import { parseStackTrace } from '../utils/stacktrace'; import { parseStackTrace } from '../utils/stacktrace';
import { logDebug, setDebug } from '../utils/logger'; import { logDebug, setDebug } from '../utils/logger';
import { setAnonymousId as setStoredAnonymousId } from '../utils/identity';
import { ConfigManager } from './ConfigManager'; import { ConfigManager } from './ConfigManager';
import { EventBus } from './EventBus'; import { EventBus } from './EventBus';
import { EventQueue } from './EventQueue'; import { EventQueue } from './EventQueue';
import { Reporter } from './Reporter'; import { Reporter } from './Reporter';
import { PluginManager } from './PluginManager'; import { PluginManager } from './PluginManager';
import { TracingManager } from './TracingManager';
class Client implements LightClient { class Client implements LightClient {
dsn: DSNInfo; dsn: DSNInfo;
@ -18,6 +20,7 @@ class Client implements LightClient {
private queue: EventQueue; private queue: EventQueue;
private reporter: Reporter; private reporter: Reporter;
private pluginManager: PluginManager; private pluginManager: PluginManager;
private tracingManager: TracingManager;
private breadcrumbs: Breadcrumb[] = []; private breadcrumbs: Breadcrumb[] = [];
private enabled: boolean = true; private enabled: boolean = true;
@ -46,6 +49,7 @@ class Client implements LightClient {
this.configManager.get('retryDelay') ?? 1000 this.configManager.get('retryDelay') ?? 1000
); );
this.pluginManager = new PluginManager(this); this.pluginManager = new PluginManager(this);
this.tracingManager = new TracingManager(config, this, this.dsn.publicKey);
this.queue = new EventQueue( this.queue = new EventQueue(
this.configManager.get('maxQueueSize') ?? 100, this.configManager.get('maxQueueSize') ?? 100,
this.configManager.get('flushInterval') ?? 5000, this.configManager.get('flushInterval') ?? 5000,
@ -175,6 +179,7 @@ class Client implements LightClient {
return; return;
} }
this.attachTraceContext(errorEvent);
errorEvent.breadcrumbs = [...this.breadcrumbs]; errorEvent.breadcrumbs = [...this.breadcrumbs];
logDebug('Client', 'Enqueue error event', { breadcrumbs: this.breadcrumbs.length }); logDebug('Client', 'Enqueue error event', { breadcrumbs: this.breadcrumbs.length });
this.queue.enqueue(errorEvent); this.queue.enqueue(errorEvent);
@ -193,6 +198,7 @@ class Client implements LightClient {
if (this.configManager.isIgnoredError(message)) return; if (this.configManager.isIgnoredError(message)) return;
this.attachTraceContext(event);
this.queue.enqueue(event); this.queue.enqueue(event);
} }
@ -206,9 +212,34 @@ class Client implements LightClient {
...event, ...event,
} as SentryEvent; } as SentryEvent;
this.attachTraceContext(fullEvent);
this.queue.enqueue(fullEvent); this.queue.enqueue(fullEvent);
} }
private attachTraceContext(event: SentryEvent): void {
const currentSpan = this.tracingManager.getCurrentSpan();
const currentTransaction = this.tracingManager.getCurrentTransaction();
if (currentSpan || currentTransaction) {
const traceId = currentSpan?.traceId || currentTransaction?.traceId;
const spanId = currentSpan?.spanId || currentTransaction?.spanId;
const parentSpanId = currentSpan?.parentSpanId;
const op = currentSpan?.op || currentTransaction?.op;
const status = currentSpan?.status || currentTransaction?.status;
event.contexts = {
...event.contexts,
trace: {
trace_id: traceId || '',
span_id: spanId || '',
parent_span_id: parentSpanId,
op,
status,
},
};
}
}
private buildErrorEvent(error: Error | unknown): ErrorEvent { private buildErrorEvent(error: Error | unknown): ErrorEvent {
const timestamp = now(); const timestamp = now();
@ -243,6 +274,42 @@ class Client implements LightClient {
setUser(user: UserInfo | null): void { setUser(user: UserInfo | null): void {
this.configManager.setUser(user); this.configManager.setUser(user);
this.tracingManager.updateConfig(this.configManager.getAll());
}
setAnonymousId(id: string): void {
setStoredAnonymousId(id);
}
startTransaction(context: TransactionContext): Transaction | null {
if (!this.enabled) return null;
const transaction = this.tracingManager.startTransaction(context);
if (transaction && transaction.sampled) {
logDebug('Client', 'Transaction started and sampled', {
name: transaction.name,
traceId: transaction.traceId,
});
}
return transaction;
}
getCurrentTransaction(): Transaction | null {
return this.tracingManager.getCurrentTransaction();
}
getCurrentSpan(): Span | null {
return this.tracingManager.getCurrentSpan();
}
setSpan(span: Span | null): void {
this.tracingManager.setCurrentSpan(span);
}
startSpan(context: SpanContext): Span | null {
if (!this.enabled) return null;
return this.tracingManager.startSpan(context);
} }
setTag(key: string, value: string): void { setTag(key: string, value: string): void {
@ -286,8 +353,9 @@ class Client implements LightClient {
} }
destroy(): void { destroy(): void {
this.queue.destroy();
this.pluginManager.destroy(); this.pluginManager.destroy();
this.tracingManager.destroy();
this.queue.destroy();
this.eventBus.destroy(); this.eventBus.destroy();
this.breadcrumbs = []; this.breadcrumbs = [];
} }

View File

@ -1,6 +1,7 @@
import type { LightConfig, SentryEvent, UserInfo, ContextLevelMap, ErrorEvent, StackFrame, Breadcrumb, EventLevel } from '../types'; import type { LightConfig, SentryEvent, UserInfo, ContextLevelMap, ErrorEvent, StackFrame, Breadcrumb, EventLevel } from '../types';
import { getContexts, getContextId, getPageUrl, getReferrer } from '../utils/env'; import { getContexts, getContextId, getPageUrl, getReferrer } from '../utils/env';
import { matchPatterns } from '../utils/helper'; import { matchPatterns } from '../utils/helper';
import { getAnonymousId, isAnonymousTrackingEnabled } from '../utils/identity';
const DEFAULT_CONFIG: Partial<LightConfig> = { const DEFAULT_CONFIG: Partial<LightConfig> = {
enabled: true, enabled: true,
@ -72,6 +73,13 @@ export class ConfigManager {
return matchPatterns(message, this.config.ignoreErrors || []); return matchPatterns(message, this.config.ignoreErrors || []);
} }
shouldSample(): boolean {
const rate = this.config.sampleRate ?? 1;
if (rate >= 1) return true;
if (rate <= 0) return false;
return Math.random() < rate;
}
/** /**
* *
* env.ts getContexts * env.ts getContexts
@ -135,6 +143,7 @@ export class ConfigManager {
if (this.config.environment) { if (this.config.environment) {
enriched.environment = this.config.environment; enriched.environment = this.config.environment;
} }
enriched.platform = 'javascript';
if (this.config.user) { if (this.config.user) {
enriched.user = this.config.user; enriched.user = this.config.user;
} }
@ -156,6 +165,16 @@ export class ConfigManager {
} }
enriched.context_id = this.contextId; enriched.context_id = this.contextId;
// 处理匿名用户ID
// 只有在没有设置 user.id 且启用了匿名追踪时,才添加 anonymousId
const hasUserId = this.config.user?.id || (event.user as Record<string, unknown>)?.id;
if (!hasUserId && isAnonymousTrackingEnabled(this.config)) {
const anonymousId = getAnonymousId();
if (anonymousId) {
enriched.anonymousId = anonymousId;
}
}
const result = this.applyContextLevel(enriched); const result = this.applyContextLevel(enriched);
if (includeFullContext && !this.contextReported) { if (includeFullContext && !this.contextReported) {

View File

@ -183,8 +183,7 @@ export class EventQueue {
} }
private cleanupExpiredDedupeEntries(): void { private cleanupExpiredDedupeEntries(): void {
// 每 100 条或每 10 次清理一次,避免过于频繁 if (Math.random() > 0.1) return;
if (this.dedupeMap.size < 100 && Math.random() > 0.1) return;
const currentTime = now(); const currentTime = now();
const expiryTime = 60000; const expiryTime = 60000;
@ -310,6 +309,7 @@ export class EventQueue {
window.removeEventListener('pagehide', this.onPagehide); window.removeEventListener('pagehide', this.onPagehide);
} }
} }
this.queue = [];
this.dedupeMap.clear(); this.dedupeMap.clear();
this.pendingCounts.clear(); this.pendingCounts.clear();
this.errorRateWindow = {}; this.errorRateWindow = {};

View File

@ -238,8 +238,6 @@ export class Reporter {
switch (event.type) { switch (event.type) {
case 'error': case 'error':
return 'event'; return 'event';
case 'performance':
return 'transaction';
default: default:
return event.type; return event.type;
} }

198
src/core/TracingManager.ts Normal file
View File

@ -0,0 +1,198 @@
import type { LightConfig, SpanContext, TransactionContext, Span, Transaction, LightClient, SamplingContext } from '../types';
import { Span as SpanImpl, Transaction as TransactionImpl } from '../utils/tracing';
import { logDebug } from '../utils/logger';
export class TracingManager {
private config: LightConfig;
private _client?: LightClient;
private currentTransaction: Transaction | null = null;
private spanStack: Span[] = [];
private dsnPublicKey?: string;
constructor(config: LightConfig, client?: LightClient, dsnPublicKey?: string) {
this.config = config;
this._client = client;
this.dsnPublicKey = dsnPublicKey;
}
updateConfig(config: LightConfig): void {
this.config = config;
if (this.currentTransaction) {
const txImpl = this.currentTransaction as unknown as TransactionImpl;
txImpl.setUserBaggage(config.user?.id, (config.user as { segment?: string } | undefined)?.segment);
}
}
private shouldSample(context: TransactionContext, parentSampled?: boolean): boolean {
const sampler = this.config.tracesSampler;
if (sampler) {
const samplingContext: SamplingContext = {
...context,
parentSampled,
};
const result = sampler(samplingContext);
if (typeof result === 'boolean') {
return result;
}
const clampedRate = Math.max(0, Math.min(1, result));
if (clampedRate === 0) return false;
if (clampedRate === 1) return true;
return Math.random() < clampedRate;
}
const rate = this.config.tracesSampleRate ?? 0;
if (rate >= 1) return true;
if (rate <= 0) return false;
return Math.random() < rate;
}
startTransaction(context: TransactionContext): Transaction | null {
const parentTx = this.currentTransaction;
const parentSampled = parentTx ? parentTx.sampled : undefined;
const sampled = context.sampled ?? this.shouldSample(context, parentSampled);
const transaction = new TransactionImpl(
{
...context,
sampled,
},
this._client,
this.dsnPublicKey,
this.config.release,
this.config.environment
) as unknown as Transaction;
this.currentTransaction = transaction;
this.spanStack = [transaction];
logDebug('TracingManager', 'Transaction started', {
name: transaction.name,
traceId: transaction.traceId,
spanId: transaction.spanId,
sampled,
op: transaction.op,
});
return transaction;
}
getCurrentTransaction(): Transaction | null {
return this.currentTransaction;
}
setCurrentTransaction(transaction: Transaction | null): void {
this.currentTransaction = transaction;
if (!transaction) {
this.spanStack = [];
} else {
this.spanStack = [transaction];
}
}
getCurrentSpan(): Span | null {
return this.spanStack.length > 0 ? this.spanStack[this.spanStack.length - 1] : null;
}
setCurrentSpan(span: Span | null): void {
if (span === null) {
if (this.spanStack.length > 1) {
this.spanStack.pop();
}
} else {
this.spanStack.push(span);
}
}
startSpan(context: SpanContext): Span | null {
const parentSpan = this.getCurrentSpan();
if (!parentSpan && !context.traceId) {
logDebug('TracingManager', 'startSpan skipped: no active span and no traceId');
return null;
}
let span: Span;
if (parentSpan) {
span = (parentSpan as SpanImpl).startChild(context);
} else {
span = new SpanImpl({
...context,
parentSpanId: context.parentSpanId,
}) as unknown as Span;
if (this.currentTransaction) {
if (this.currentTransaction.sampled !== false) {
(this.currentTransaction as unknown as TransactionImpl)._addSpan(span as unknown as SpanImpl);
}
(span as SpanImpl & { transaction: Transaction }).transaction = this.currentTransaction;
}
}
this.spanStack.push(span);
logDebug('TracingManager', 'Span started', {
op: span.op,
description: span.description,
spanId: span.spanId,
parentSpanId: span.parentSpanId,
stackDepth: this.spanStack.length,
});
return span;
}
onSpanFinished(span: Span): void {
const idx = this.spanStack.lastIndexOf(span);
if (idx !== -1) {
this.spanStack.splice(idx, 1);
}
if (this.spanStack.length === 0 && this.currentTransaction) {
this.spanStack = [this.currentTransaction];
}
logDebug('TracingManager', 'Span finished', {
op: span.op,
description: span.description,
spanId: span.spanId,
stackDepth: this.spanStack.length,
});
}
onTransactionFinished(transaction: Transaction): void {
if (this.currentTransaction === transaction) {
this.currentTransaction = null;
this.spanStack = [];
}
logDebug('TracingManager', 'Transaction finished', {
name: transaction.name,
traceId: transaction.traceId,
spanCount: transaction.spans.length,
sampled: transaction.sampled,
});
}
finishSpan(span: Span): void {
span.finish();
}
finishTransaction(transaction: Transaction): void {
transaction.finish();
}
destroy(): void {
if (this.currentTransaction) {
try {
this.currentTransaction.setStatus('aborted');
this.currentTransaction.finish();
} catch {
// ignore
}
}
this.currentTransaction = null;
this.spanStack = [];
}
}

View File

@ -5,8 +5,13 @@ import NetworkPlugin from './plugins/NetworkPlugin';
import BehaviorPlugin from './plugins/BehaviorPlugin'; import BehaviorPlugin from './plugins/BehaviorPlugin';
import OfflinePlugin from './plugins/OfflinePlugin'; import OfflinePlugin from './plugins/OfflinePlugin';
import SamplingPlugin from './plugins/SamplingPlugin'; import SamplingPlugin from './plugins/SamplingPlugin';
import type { LightConfig, LightClient, SentryEvent, EventLevel, UserInfo, Breadcrumb, LightPlugin, ErrorEvent, PerformanceEvent, NetworkEvent, BehaviorEvent, DSNInfo } from './types'; import RouterPlugin from './plugins/RouterPlugin';
import InteractionPlugin from './plugins/InteractionPlugin';
import { Span as SpanClass, Transaction as TransactionClass, httpStatusToSpanStatus, dbStatusToSpanStatus, statusToSpanStatus } from './utils/tracing';
import { serializeBaggage, parseBaggage, createBaggageFromTransaction } from './utils/baggage';
import type { LightConfig, LightClient, SentryEvent, EventLevel, UserInfo, Breadcrumb, LightPlugin, ErrorEvent, PerformanceEvent, NetworkEvent, BehaviorEvent, DSNInfo, TransactionContext, SpanContext, Transaction, Span, SamplingContext, BaggageFields, SpanStatus } from './types';
import { logDebug, setDebug } from './utils/logger'; import { logDebug, setDebug } from './utils/logger';
import { setAnonymousId as setStoredAnonymousId, clearAnonymousId as clearStoredAnonymousId, getAnonymousId as getStoredAnonymousId } from './utils/identity';
let globalClient: LightClient | null = null; let globalClient: LightClient | null = null;
@ -33,6 +38,12 @@ function init(config: LightConfig): LightClient {
if (pluginNames.includes('behavior') || pluginNames.includes('all')) { if (pluginNames.includes('behavior') || pluginNames.includes('all')) {
defaultPlugins.push(new BehaviorPlugin()); defaultPlugins.push(new BehaviorPlugin());
} }
if (pluginNames.includes('router') || pluginNames.includes('all')) {
defaultPlugins.push(new RouterPlugin());
}
if (pluginNames.includes('interaction') || pluginNames.includes('all')) {
defaultPlugins.push(new InteractionPlugin());
}
logDebug('Index', 'Registering plugins', { count: defaultPlugins.length, plugins: defaultPlugins.map(p => p.name) }); logDebug('Index', 'Registering plugins', { count: defaultPlugins.length, plugins: defaultPlugins.map(p => p.name) });
@ -90,6 +101,38 @@ function setUser(user: UserInfo | null): void {
globalClient?.setUser(user); globalClient?.setUser(user);
} }
function setAnonymousId(id: string): void {
setStoredAnonymousId(id);
}
function clearAnonymousId(): void {
clearStoredAnonymousId();
}
function getAnonymousId(): string | null {
return getStoredAnonymousId();
}
function startTransaction(context: TransactionContext): Transaction | null {
return globalClient?.startTransaction(context) || null;
}
function getCurrentTransaction(): Transaction | null {
return globalClient?.getCurrentTransaction() || null;
}
function getCurrentSpan(): Span | null {
return globalClient?.getCurrentSpan() || null;
}
function setSpan(span: Span | null): void {
globalClient?.setSpan(span);
}
function startSpan(context: SpanContext): Span | null {
return globalClient?.startSpan(context) || null;
}
function setTag(key: string, value: string): void { function setTag(key: string, value: string): void {
globalClient?.setTag(key, value); globalClient?.setTag(key, value);
} }
@ -125,6 +168,14 @@ export {
captureMessage, captureMessage,
captureEvent, captureEvent,
setUser, setUser,
setAnonymousId,
clearAnonymousId,
getAnonymousId,
startTransaction,
getCurrentTransaction,
getCurrentSpan,
setSpan,
startSpan,
setTag, setTag,
setTags, setTags,
setExtra, setExtra,
@ -133,14 +184,24 @@ export {
disable, disable,
enable, enable,
Client, Client,
httpStatusToSpanStatus,
dbStatusToSpanStatus,
statusToSpanStatus,
serializeBaggage,
parseBaggage,
createBaggageFromTransaction,
ErrorPlugin, ErrorPlugin,
PerformancePlugin, PerformancePlugin,
NetworkPlugin, NetworkPlugin,
BehaviorPlugin, BehaviorPlugin,
OfflinePlugin, OfflinePlugin,
SamplingPlugin, SamplingPlugin,
RouterPlugin,
InteractionPlugin,
}; };
export { SpanClass as Span, TransactionClass as Transaction };
export type { export type {
LightConfig, LightConfig,
LightClient, LightClient,
@ -154,6 +215,13 @@ export type {
UserInfo, UserInfo,
Breadcrumb, Breadcrumb,
DSNInfo, DSNInfo,
TransactionContext,
SpanContext,
SamplingContext,
BaggageFields,
SpanStatus,
SpanJSON,
TransactionJSON,
}; };
export default { export default {
@ -163,6 +231,14 @@ export default {
captureMessage, captureMessage,
captureEvent, captureEvent,
setUser, setUser,
setAnonymousId,
clearAnonymousId,
getAnonymousId,
startTransaction,
getCurrentTransaction,
getCurrentSpan,
setSpan,
startSpan,
setTag, setTag,
setTags, setTags,
setExtra, setExtra,
@ -170,4 +246,7 @@ export default {
flush, flush,
disable, disable,
enable, enable,
Span: SpanClass,
Transaction: TransactionClass,
httpStatusToSpanStatus,
}; };

View File

@ -3,6 +3,7 @@ import { now, isObject, matchPatterns } from '../utils/helper';
import { computeFingerprint } from '../utils/hash'; import { computeFingerprint } from '../utils/hash';
import { sanitizeUrl, shouldIgnoreUrl } from '../utils/env'; import { sanitizeUrl, shouldIgnoreUrl } from '../utils/env';
import { parseStackTrace } from '../utils/stacktrace'; import { parseStackTrace } from '../utils/stacktrace';
import { logError } from '../utils/logger';
interface ErrorPluginConfig { interface ErrorPluginConfig {
maxStackFrames?: number; maxStackFrames?: number;
@ -120,7 +121,7 @@ class ErrorPlugin implements LightPlugin {
this.client.captureEvent(errorEvent); this.client.captureEvent(errorEvent);
} }
} catch (e) { } catch (e) {
console.error('[LightSDK] ErrorPlugin global error handler error:', e); logError('ErrorPlugin', 'Global error handler error', e);
} }
return false; return false;
@ -153,7 +154,7 @@ class ErrorPlugin implements LightPlugin {
this.client.captureEvent(errorEvent); this.client.captureEvent(errorEvent);
} }
} catch (e) { } catch (e) {
console.error('[LightSDK] ErrorPlugin unhandledrejection handler error:', e); logError('ErrorPlugin', 'Unhandledrejection handler error', e);
} }
}; };
@ -207,7 +208,7 @@ class ErrorPlugin implements LightPlugin {
}, },
}); });
} catch (e) { } catch (e) {
console.error('[LightSDK] ErrorPlugin resource error handler error:', e); logError('ErrorPlugin', 'Resource error handler error', e);
} }
} }
}; };

View File

@ -0,0 +1,207 @@
import type { LightPlugin, LightClient, Transaction, Span, SpanStatus, LightConfig } from '../types';
import { now, isObject } from '../utils/helper';
interface InteractionPluginConfig {
enable?: boolean;
elementAttribute?: string;
maxTransactionDurationMs?: number;
idleTimeoutMs?: number;
captureScroll?: boolean;
trackClick?: boolean;
trackSubmit?: boolean;
trackKeypress?: boolean;
}
const DEFAULT_TRACKING_TYPES = ['click', 'submit'];
class InteractionPlugin implements LightPlugin {
name = 'interaction';
version = '1.0.0';
private client!: LightClient;
private config: InteractionPluginConfig = {
enable: true,
elementAttribute: 'data-sentry-component',
maxTransactionDurationMs: 30000,
idleTimeoutMs: 1000,
trackClick: true,
trackSubmit: true,
trackKeypress: false,
};
private currentTransaction: Transaction | null = null;
private idleTimer: ReturnType<typeof setTimeout> | null = null;
private maxDurationTimer: ReturnType<typeof setTimeout> | null = null;
private debounceTimer: ReturnType<typeof setTimeout> | null = null;
setup(client: LightClient): void {
this.client = client;
this.loadConfig();
if (typeof window === 'undefined') return;
if (!this.config.enable) return;
if (this.config.trackClick) {
document.addEventListener('click', this.handleClick, true);
}
if (this.config.trackSubmit) {
document.addEventListener('submit', this.handleSubmit, true);
}
}
private loadConfig(): void {
const clientConfig = this.client.config as LightConfig;
if (clientConfig && isObject((clientConfig as Record<string, unknown>).interaction)) {
this.config = { ...this.config, ...((clientConfig as Record<string, unknown>).interaction as object) };
}
}
private handleClick = (event: Event): void => {
if (this.isEventIgnored(event)) return;
const target = event.target as HTMLElement;
const elementName = this.getElementName(target, 'click');
this.scheduleInteractionTransaction(elementName, 'click', target);
};
private handleSubmit = (event: Event): void => {
if (this.isEventIgnored(event)) return;
const target = event.target as HTMLElement;
const elementName = this.getElementName(target, 'submit');
this.scheduleInteractionTransaction(elementName, 'submit', target);
};
private isEventIgnored(event: Event): boolean {
const target = event.target as HTMLElement;
if (!target) return true;
if (target.hasAttribute && target.hasAttribute('data-sentry-ignore')) {
return true;
}
return false;
}
private scheduleInteractionTransaction(name: string, op: string, element: HTMLElement): void {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = setTimeout(() => {
this.startInteractionTransaction(name, op, element);
}, 0);
}
private getElementName(element: HTMLElement | null, eventType: string): string {
if (!element) return eventType;
const attr = this.config.elementAttribute || 'data-sentry-component';
const componentName = element.getAttribute(attr);
if (componentName) return componentName;
const tag = element.tagName?.toLowerCase() || 'element';
const id = element.id ? `#${element.id}` : '';
const classes = element.className && typeof element.className === 'string'
? `.${element.className.split(' ').slice(0, 3).join('.')}`
: '';
return `${eventType}@${tag}${id}${classes}`;
}
private startInteractionTransaction(name: string, op: string, element: HTMLElement): void {
if (!this.client) return;
if (this.currentTransaction) {
this.finishCurrentTransaction('ok');
}
const transaction = this.client.startTransaction({
name,
op: `ui.${op}`,
description: `User ${op} on ${name}`,
type: 'user-interaction',
tags: {
'interaction.type': op,
'interaction.target': name,
},
data: {
tag: element.tagName?.toLowerCase(),
id: element.id || undefined,
text: element.textContent?.substring(0, 100) || undefined,
},
});
if (transaction) {
this.currentTransaction = transaction;
this.scheduleIdleFinish();
this.scheduleMaxDuration();
}
}
private scheduleIdleFinish(): void {
if (this.idleTimer) {
clearTimeout(this.idleTimer);
}
const idleTimeout = this.config.idleTimeoutMs ?? 1000;
this.idleTimer = setTimeout(() => {
this.finishCurrentTransaction('ok');
}, idleTimeout);
}
private scheduleMaxDuration(): void {
if (this.maxDurationTimer) {
clearTimeout(this.maxDurationTimer);
}
const maxDuration = this.config.maxTransactionDurationMs ?? 30000;
this.maxDurationTimer = setTimeout(() => {
this.finishCurrentTransaction('deadline_exceeded');
}, maxDuration);
}
private finishCurrentTransaction(status: SpanStatus = 'ok'): void {
if (this.idleTimer) {
clearTimeout(this.idleTimer);
this.idleTimer = null;
}
if (this.maxDurationTimer) {
clearTimeout(this.maxDurationTimer);
this.maxDurationTimer = null;
}
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
this.debounceTimer = null;
}
if (this.currentTransaction) {
this.currentTransaction.setStatus(status);
this.currentTransaction.finish();
this.currentTransaction = null;
}
}
destroy(): void {
if (typeof document !== 'undefined') {
document.removeEventListener('click', this.handleClick, true);
document.removeEventListener('submit', this.handleSubmit, true);
}
if (this.idleTimer) {
clearTimeout(this.idleTimer);
this.idleTimer = null;
}
if (this.maxDurationTimer) {
clearTimeout(this.maxDurationTimer);
this.maxDurationTimer = null;
}
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
this.debounceTimer = null;
}
this.finishCurrentTransaction('aborted');
}
}
export default InteractionPlugin;

View File

@ -1,4 +1,4 @@
import type { LightPlugin, LightClient, NetworkEvent, LightConfig } from '../types'; import type { LightPlugin, LightClient, NetworkEvent, LightConfig, Span, Transaction } from '../types';
import { now, isObject } from '../utils/helper'; import { now, isObject } from '../utils/helper';
import { sanitizeUrl, shouldIgnoreUrl } from '../utils/env'; import { sanitizeUrl, shouldIgnoreUrl } from '../utils/env';
@ -17,11 +17,12 @@ interface NetworkPluginConfig {
slowThreshold?: number; slowThreshold?: number;
slowSampleRate?: number; slowSampleRate?: number;
ignoreStatusCodes?: number[]; ignoreStatusCodes?: number[];
tracePropagationTargets?: (string | RegExp)[];
} }
class NetworkPlugin implements LightPlugin { class NetworkPlugin implements LightPlugin {
name = 'network'; name = 'network';
version = '1.0.0'; version = '1.1.0';
private client!: LightClient; private client!: LightClient;
private config: NetworkPluginConfig = { private config: NetworkPluginConfig = {
@ -63,12 +64,63 @@ class NetworkPlugin implements LightPlugin {
if (clientConfig.ignoreUrls) { if (clientConfig.ignoreUrls) {
this.config.ignoreUrls = [...(this.config.ignoreUrls || []), ...clientConfig.ignoreUrls]; this.config.ignoreUrls = [...(this.config.ignoreUrls || []), ...clientConfig.ignoreUrls];
} }
if (clientConfig.tracePropagationTargets) {
this.config.tracePropagationTargets = [...(this.config.tracePropagationTargets || []), ...clientConfig.tracePropagationTargets];
} else if (clientConfig.tracingOrigins) {
this.config.tracePropagationTargets = [...(this.config.tracePropagationTargets || []), ...clientConfig.tracingOrigins];
}
} }
private shouldIgnore(url: string): boolean { private shouldIgnore(url: string): boolean {
return shouldIgnoreUrl(url, this.config.ignoreUrls || []); return shouldIgnoreUrl(url, this.config.ignoreUrls || []);
} }
private shouldTracePropagate(url: string): boolean {
const targets = this.config.tracePropagationTargets;
if (!targets || targets.length === 0) {
return this.isSameOrigin(url);
}
return targets.some(pattern => {
if (typeof pattern === 'string') {
return url.includes(pattern);
}
return pattern.test(url);
});
}
private isSameOrigin(url: string): boolean {
if (typeof location === 'undefined') return false;
try {
const urlObj = new URL(url, location.origin);
return urlObj.origin === location.origin;
} catch {
return false;
}
}
private getBaggageHeader(): string | undefined {
if (!this.client) return undefined;
const clientInternal = this.client as unknown as {
tracingManager?: {
getCurrentTransaction?: () => Transaction | null;
};
};
const tx = clientInternal.tracingManager?.getCurrentTransaction?.();
if (!tx) return undefined;
const transactionImpl = tx as unknown as {
toBaggage?: () => string | undefined;
};
if (transactionImpl.toBaggage) {
return transactionImpl.toBaggage();
}
return undefined;
}
private shouldSample(isError: boolean, duration: number): boolean { private shouldSample(isError: boolean, duration: number): boolean {
let rate = isError ? (this.config.errorSampleRate ?? 1.0) : (this.config.successSampleRate ?? 0.01); let rate = isError ? (this.config.errorSampleRate ?? 1.0) : (this.config.successSampleRate ?? 0.01);
@ -81,6 +133,34 @@ class NetworkPlugin implements LightPlugin {
return Math.random() < rate; return Math.random() < rate;
} }
private createHttpSpan(method: string, url: string): Span | null {
const currentSpan = this.client.getCurrentSpan();
if (!currentSpan) return null;
const span = currentSpan.startChild({
op: 'http.client',
description: `${method.toUpperCase()} ${url}`,
tags: {
'http.method': method.toUpperCase(),
'http.url': sanitizeUrl(url),
},
});
return span;
}
private finishHttpSpan(span: Span, statusCode: number, duration: number, isError: boolean): void {
span.setData('http.response.duration', duration);
if (statusCode > 0) {
span.setHttpStatus(statusCode);
} else if (isError) {
span.setStatus('internal_error');
}
span.finish();
}
private patchFetch(): void { private patchFetch(): void {
if (typeof fetch === 'undefined') return; if (typeof fetch === 'undefined') return;
@ -97,7 +177,27 @@ class NetworkPlugin implements LightPlugin {
} }
const sanitizedUrl = sanitizeUrl(url); const sanitizedUrl = sanitizeUrl(url);
const promise = self.originalFetch.call(window, input, init);
let httpSpan = self.createHttpSpan(method, sanitizedUrl);
const shouldInjectTrace = self.shouldTracePropagate(url) && !!httpSpan;
let modifiedInit = init;
if (shouldInjectTrace && httpSpan) {
const traceHeader = httpSpan.toTraceparent();
const baggage = self.getBaggageHeader();
modifiedInit = {
...init,
headers: {
...(init?.headers || {}),
'sentry-trace': traceHeader,
...(baggage ? { 'baggage': baggage } : {}),
},
};
}
const promise = self.originalFetch.call(window, input, modifiedInit);
promise.then( promise.then(
(response) => { (response) => {
@ -105,6 +205,10 @@ class NetworkPlugin implements LightPlugin {
const isSuccess = response.ok; const isSuccess = response.ok;
const statusCode = response.status; const statusCode = response.status;
if (httpSpan) {
self.finishHttpSpan(httpSpan, statusCode, duration, !isSuccess);
}
if (self.config.ignoreStatusCodes?.includes(statusCode)) { if (self.config.ignoreStatusCodes?.includes(statusCode)) {
return response; return response;
} }
@ -130,6 +234,10 @@ class NetworkPlugin implements LightPlugin {
(error) => { (error) => {
const duration = now() - startTime; const duration = now() - startTime;
if (httpSpan) {
self.finishHttpSpan(httpSpan, 0, duration, true);
}
if (self.shouldSample(true, duration)) { if (self.shouldSample(true, duration)) {
self.reportNetwork({ self.reportNetwork({
sub_type: 'fetch', sub_type: 'fetch',
@ -180,12 +288,32 @@ class NetworkPlugin implements LightPlugin {
requestSize = body.length; requestSize = body.length;
} }
const httpSpan = self.createHttpSpan(method, sanitizedUrl);
const shouldInjectTrace = self.shouldTracePropagate(url) && !!httpSpan;
if (shouldInjectTrace && httpSpan) {
try {
const traceHeader = httpSpan.toTraceparent();
const baggage = self.getBaggageHeader();
this.setRequestHeader('sentry-trace', traceHeader);
if (baggage) {
this.setRequestHeader('baggage', baggage);
}
} catch {
// ignore
}
}
const originalOnLoadEnd = (this as XMLHttpRequest & { onloadend?: ((this: XMLHttpRequest, ev: ProgressEvent) => unknown) | null }).onloadend; const originalOnLoadEnd = (this as XMLHttpRequest & { onloadend?: ((this: XMLHttpRequest, ev: ProgressEvent) => unknown) | null }).onloadend;
(this as XMLHttpRequest & { onloadend?: ((this: XMLHttpRequest, ev: ProgressEvent) => unknown) | null }).onloadend = function (ev: ProgressEvent) { (this as XMLHttpRequest & { onloadend?: ((this: XMLHttpRequest, ev: ProgressEvent) => unknown) | null }).onloadend = function (ev: ProgressEvent) {
const duration = now() - startTime; const duration = now() - startTime;
const status = this.status; const status = this.status;
const isSuccess = status >= 200 && status < 300; const isSuccess = status >= 200 && status < 300;
if (httpSpan) {
self.finishHttpSpan(httpSpan, status, duration, !isSuccess);
}
if (!self.config.ignoreStatusCodes?.includes(status) if (!self.config.ignoreStatusCodes?.includes(status)
&& self.shouldSample(!isSuccess, duration) && self.shouldSample(!isSuccess, duration)
&& (isSuccess ? self.config.captureSuccess : true)) { && (isSuccess ? self.config.captureSuccess : true)) {

View File

@ -1,4 +1,4 @@
import type { LightPlugin, LightClient, PerformanceEvent, LightConfig } from '../types'; import type { LightPlugin, LightClient, PerformanceEvent, LightConfig, Transaction, Span } from '../types';
import { now, isObject } from '../utils/helper'; import { now, isObject } from '../utils/helper';
interface PerformancePluginConfig { interface PerformancePluginConfig {
@ -9,11 +9,19 @@ interface PerformancePluginConfig {
longTaskSampleRate?: number; longTaskSampleRate?: number;
captureNavigation?: boolean; captureNavigation?: boolean;
captureFP?: boolean; captureFP?: boolean;
enablePageLoadTracing?: boolean; // 是否启用 Page Load Transaction
maxResourceSpans?: number; // 最大 Resource Span 数量
}
interface VitalMeasurement {
value: number;
unit?: string;
rating?: 'good' | 'needs-improvement' | 'poor';
} }
class PerformancePlugin implements LightPlugin { class PerformancePlugin implements LightPlugin {
name = 'performance'; name = 'performance';
version = '1.0.0'; version = '2.0.0';
private client!: LightClient; private client!: LightClient;
private config: PerformancePluginConfig = { private config: PerformancePluginConfig = {
@ -24,6 +32,8 @@ class PerformancePlugin implements LightPlugin {
longTaskSampleRate: 0.05, longTaskSampleRate: 0.05,
captureNavigation: true, captureNavigation: true,
captureFP: true, captureFP: true,
enablePageLoadTracing: false,
maxResourceSpans: 50,
}; };
private observers: PerformanceObserver[] = []; private observers: PerformanceObserver[] = [];
@ -35,16 +45,44 @@ class PerformancePlugin implements LightPlugin {
private ttiReported: boolean = false; private ttiReported: boolean = false;
private ttiTimer: ReturnType<typeof setTimeout> | null = null; private ttiTimer: ReturnType<typeof setTimeout> | null = null;
private pageLoadTransaction: Transaction | null = null;
private measurements: Record<string, VitalMeasurement> = {};
private resourceSpanCount: number = 0;
setup(client: LightClient): void { setup(client: LightClient): void {
this.client = client; this.client = client;
this.loadConfig(); this.loadConfig();
if (this.config.enablePageLoadTracing) {
this.startPageLoadTransaction();
}
this.observeWebVitals(); this.observeWebVitals();
this.observeLongTasks(); this.observeLongTasks();
this.observeNavigation(); this.observeNavigation();
this.observeResources(); this.observeResources();
} }
private startPageLoadTransaction(): void {
if (typeof window === 'undefined') return;
const pageUrl = typeof location !== 'undefined' ? location.pathname : '/';
const navStart = typeof performance !== 'undefined' && performance.timing?.navigationStart
? performance.timing.navigationStart
: now();
this.pageLoadTransaction = this.client.startTransaction({
name: pageUrl,
op: 'pageload',
description: `Page load - ${pageUrl}`,
startTimestamp: navStart,
type: 'pageload',
tags: {
'page.url': pageUrl,
},
});
}
private loadConfig(): void { private loadConfig(): void {
const clientConfig = this.client.config as LightConfig; const clientConfig = this.client.config as LightConfig;
if (clientConfig && isObject(clientConfig.performance)) { if (clientConfig && isObject(clientConfig.performance)) {
@ -69,28 +107,32 @@ class PerformancePlugin implements LightPlugin {
private observeWebVitals(): void { private observeWebVitals(): void {
if (typeof PerformanceObserver === 'undefined') return; if (typeof PerformanceObserver === 'undefined') return;
if (this.config.captureFP) { this.observePaint();
this.observeFP();
}
this.observeLCP(); this.observeLCP();
this.observeFID(); this.observeFID();
this.observeCLS(); this.observeCLS();
this.observeFCP();
this.observeTTFB(); this.observeTTFB();
} }
private observeFP(): void { private observePaint(): void {
try { try {
const po = new PerformanceObserver((entryList) => { const po = new PerformanceObserver((entryList) => {
const entries = entryList.getEntriesByName('first-paint'); const fpEntries = entryList.getEntriesByName('first-paint');
if (entries.length > 0) { if (fpEntries.length > 0 && this.config.captureFP) {
this.reportMetric('FP', entries[0].startTime, 'ms', {}, 'vitals'); this.reportMetric('FP', fpEntries[0].startTime, 'ms', {}, 'vitals');
}
const fcpEntries = entryList.getEntriesByName('first-contentful-paint');
if (fcpEntries.length > 0) {
this.fcpTime = fcpEntries[0].startTime;
this.lastLongTaskEndTime = fcpEntries[0].startTime;
this.reportMetric('FCP', fcpEntries[0].startTime, 'ms', {}, 'vitals');
this.scheduleTTI();
} }
}); });
po.observe({ type: 'paint', buffered: true }); po.observe({ type: 'paint', buffered: true });
this.observers.push(po); this.observers.push(po);
} catch { } catch {
// FP not supported // Paint observer not supported
} }
} }
@ -167,24 +209,6 @@ class PerformancePlugin implements LightPlugin {
} }
} }
private observeFCP(): void {
try {
const po = new PerformanceObserver((entryList) => {
const entries = entryList.getEntriesByName('first-contentful-paint');
if (entries.length > 0) {
this.fcpTime = entries[0].startTime;
this.lastLongTaskEndTime = entries[0].startTime;
this.reportMetric('FCP', entries[0].startTime, 'ms', {}, 'vitals');
this.scheduleTTI();
}
});
po.observe({ type: 'paint', buffered: true });
this.observers.push(po);
} catch {
// FCP not supported
}
}
private observeTTFB(): void { private observeTTFB(): void {
try { try {
const po = new PerformanceObserver((entryList) => { const po = new PerformanceObserver((entryList) => {
@ -276,6 +300,12 @@ class PerformancePlugin implements LightPlugin {
this.reportMetric(name, value, 'ms'); this.reportMetric(name, value, 'ms');
} }
} }
if (this.pageLoadTransaction && this.pageLoadTransaction.sampled !== false) {
this.buildNavigationSpans(timing, navigationStart);
this.finishPageLoadTransaction(timing.loadEventEnd || now());
}
this.navLoadTimer = null; this.navLoadTimer = null;
}, 0); }, 0);
}; };
@ -283,20 +313,63 @@ class PerformancePlugin implements LightPlugin {
window.addEventListener('load', this.onLoad); window.addEventListener('load', this.onLoad);
} }
private buildNavigationSpans(timing: PerformanceTiming, navigationStart: number): void {
const spanDefs: Array<{ op: string; desc: string; start: number; end: number }> = [
{ op: 'dns', desc: 'DNS Lookup', start: timing.domainLookupStart, end: timing.domainLookupEnd },
{ op: 'tcp', desc: 'TCP Connection', start: timing.connectStart, end: timing.connectEnd },
{ op: 'ssl', desc: 'SSL/TLS Negotiation', start: timing.secureConnectionStart, end: timing.connectEnd },
{ op: 'ttfb', desc: 'Time to First Byte', start: timing.requestStart, end: timing.responseStart },
{ op: 'response', desc: 'Response Download', start: timing.responseStart, end: timing.responseEnd },
{ op: 'dom_parse', desc: 'DOM Parsing', start: timing.responseEnd, end: timing.domInteractive },
{ op: 'dom_content_loaded', desc: 'DOM Content Loaded', start: timing.domLoading, end: timing.domContentLoadedEventEnd },
{ op: 'load', desc: 'Page Load', start: timing.navigationStart, end: timing.loadEventEnd },
];
for (const def of spanDefs) {
if (def.start > 0 && def.end > 0 && def.end > def.start) {
this.createNavigationSpan(
def.op,
def.desc,
def.start,
def.end
);
}
}
}
private finishPageLoadTransaction(endTime: number): void {
if (!this.pageLoadTransaction) return;
if (Object.keys(this.measurements).length > 0) {
(this.pageLoadTransaction as unknown as { setData: (k: string, v: unknown) => void }).setData(
'measurements',
this.measurements
);
}
this.pageLoadTransaction.setStatus('ok');
this.pageLoadTransaction.finish(endTime);
this.pageLoadTransaction = null;
}
private observeResources(): void { private observeResources(): void {
if (!this.config.captureResources) return; if (!this.config.captureResources && !this.config.enablePageLoadTracing) return;
if (typeof performance === 'undefined') return; if (typeof performance === 'undefined') return;
try { try {
const po = new PerformanceObserver((entryList) => { const po = new PerformanceObserver((entryList) => {
const entries = entryList.getEntriesByType('resource') as PerformanceResourceTiming[]; const entries = entryList.getEntriesByType('resource') as PerformanceResourceTiming[];
for (const entry of entries) { for (const entry of entries) {
if (entry.duration > 1000) { if (entry.duration > 1000 && this.config.captureResources) {
this.reportMetric('resource_slow', entry.duration, 'ms', { this.reportMetric('resource_slow', entry.duration, 'ms', {
resource_name: entry.name.substring(0, 200), resource_name: entry.name.substring(0, 200),
resource_type: entry.initiatorType, resource_type: entry.initiatorType,
}, 'resource'); }, 'resource');
} }
if (this.config.enablePageLoadTracing && this.pageLoadTransaction) {
this.createResourceSpan(entry);
}
} }
}); });
po.observe({ type: 'resource', buffered: true }); po.observe({ type: 'resource', buffered: true });
@ -345,6 +418,78 @@ class PerformancePlugin implements LightPlugin {
}; };
this.client.captureEvent(event); this.client.captureEvent(event);
if (this.pageLoadTransaction && this.pageLoadTransaction.sampled !== false) {
this.recordMeasurement(name, value, unit, rating);
}
}
private recordMeasurement(name: string, value: number, unit?: string, rating?: 'good' | 'needs-improvement' | 'poor'): void {
if (!this.pageLoadTransaction) return;
this.measurements[name] = {
value: Math.round(value * 100) / 100,
unit,
rating,
};
this.pageLoadTransaction.setTag(`measurement.${name}.rating`, rating || 'unknown');
}
private createNavigationSpan(
op: string,
description: string,
startTime: number,
endTime: number,
tags?: Record<string, string>
): void {
if (!this.pageLoadTransaction || this.pageLoadTransaction.sampled === false) return;
if (endTime <= startTime) return;
const span = this.pageLoadTransaction.startChild({
op: `browser.${op}`,
description,
startTimestamp: startTime,
tags,
});
span.finish(endTime);
}
private createResourceSpan(entry: PerformanceResourceTiming): void {
if (!this.pageLoadTransaction || this.pageLoadTransaction.sampled === false) return;
const maxResources = this.config.maxResourceSpans ?? 50;
if (this.resourceSpanCount >= maxResources) return;
this.resourceSpanCount++;
const timeOrigin = typeof performance !== 'undefined' && performance.timeOrigin
? performance.timeOrigin
: (performance.timing?.navigationStart || 0);
const startTime = timeOrigin + entry.startTime;
const endTime = timeOrigin + (entry.responseEnd || entry.duration || entry.startTime);
if (endTime <= startTime) return;
const url = entry.name.substring(0, 200);
const span = this.pageLoadTransaction.startChild({
op: `resource.${entry.initiatorType || 'other'}`,
description: url,
startTimestamp: startTime,
tags: {
'resource.type': entry.initiatorType || 'other',
'resource.url': url,
'resource.transfer_size': String(entry.transferSize || 0),
'resource.decoded_size': String(entry.decodedBodySize || 0),
},
data: {
transferSize: entry.transferSize,
decodedSize: entry.decodedBodySize,
encodedSize: entry.encodedBodySize,
},
});
span.finish(endTime);
} }
destroy(): void { destroy(): void {
@ -370,6 +515,15 @@ class PerformancePlugin implements LightPlugin {
if (typeof window !== 'undefined' && this.onLoad) { if (typeof window !== 'undefined' && this.onLoad) {
window.removeEventListener('load', this.onLoad); window.removeEventListener('load', this.onLoad);
} }
if (this.pageLoadTransaction) {
this.pageLoadTransaction.setStatus('internal_error');
this.pageLoadTransaction.finish();
this.pageLoadTransaction = null;
}
this.measurements = {};
this.resourceSpanCount = 0;
} }
} }

181
src/plugins/RouterPlugin.ts Normal file
View File

@ -0,0 +1,181 @@
import type { LightPlugin, LightClient, Transaction, SpanStatus, LightConfig } from '../types';
import { now, isObject } from '../utils/helper';
interface RouterPluginConfig {
enable?: boolean;
instrumentHistory?: boolean;
instrumentHash?: boolean;
routeNameFormatter?: (url: string) => string;
maxTransactionDurationMs?: number;
}
class RouterPlugin implements LightPlugin {
name = 'router';
version = '1.0.0';
private client!: LightClient;
private config: RouterPluginConfig = {
enable: true,
instrumentHistory: true,
instrumentHash: true,
maxTransactionDurationMs: 30000,
};
private currentTransaction: Transaction | null = null;
private lastRoute: string = '';
private originalPushState?: typeof history.pushState;
private originalReplaceState?: typeof history.replaceState;
private maxDurationTimer: ReturnType<typeof setTimeout> | null = null;
private routeChangeTimer: ReturnType<typeof setTimeout> | null = null;
setup(client: LightClient): void {
this.client = client;
this.loadConfig();
if (typeof window === 'undefined') return;
if (!this.config.enable) return;
if (this.config.instrumentHistory) {
this.instrumentHistory();
}
if (this.config.instrumentHash) {
this.instrumentHash();
}
}
private loadConfig(): void {
const clientConfig = this.client.config as LightConfig;
if (clientConfig && isObject((clientConfig as Record<string, unknown>).router)) {
this.config = { ...this.config, ...((clientConfig as Record<string, unknown>).router as object) };
}
}
private instrumentHistory(): void {
if (typeof history === 'undefined' || !history.pushState) return;
this.originalPushState = history.pushState.bind(history);
this.originalReplaceState = history.replaceState.bind(history);
history.pushState = (data: unknown, title: string, url?: string | null): void => {
this.originalPushState!(data, title, url);
this.handleRouteChange(url || location.pathname + location.search, 'pushState');
};
history.replaceState = (data: unknown, title: string, url?: string | null): void => {
this.originalReplaceState!(data, title, url);
this.handleRouteChange(url || location.pathname + location.search, 'replaceState');
};
window.addEventListener('popstate', this.handlePopState);
}
private handlePopState = (): void => {
this.handleRouteChange(location.pathname + location.search, 'popstate');
};
private instrumentHash(): void {
window.addEventListener('hashchange', this.handleHashChange);
}
private handleHashChange = (): void => {
this.handleRouteChange(location.hash || '#/', 'hashchange');
};
private handleRouteChange(to: string, source: string): void {
if (this.routeChangeTimer) {
clearTimeout(this.routeChangeTimer);
}
this.routeChangeTimer = setTimeout(() => {
this.routeChangeTimer = null;
this.processRouteChange(to, source);
}, 50);
}
private processRouteChange(to: string, source: string): void {
const routeName = this.config.routeNameFormatter
? this.config.routeNameFormatter(to)
: to.split('?')[0];
if (routeName === this.lastRoute) return;
this.lastRoute = routeName;
this.finishCurrentTransaction('ok');
this.startNavigationTransaction(routeName, source);
}
private startNavigationTransaction(name: string, source: string): void {
if (!this.client) return;
const transaction = this.client.startTransaction({
name,
op: 'navigation',
description: `Navigation to ${name}`,
type: 'navigation',
tags: {
'route.name': name,
'route.source': source,
},
});
if (transaction) {
this.currentTransaction = transaction;
this.startMaxDurationTimer();
}
}
private startMaxDurationTimer(): void {
if (this.maxDurationTimer) {
clearTimeout(this.maxDurationTimer);
}
const maxDuration = this.config.maxTransactionDurationMs ?? 30000;
this.maxDurationTimer = setTimeout(() => {
if (this.currentTransaction) {
this.currentTransaction.setStatus('deadline_exceeded');
this.finishCurrentTransaction('deadline_exceeded');
}
}, maxDuration);
}
private finishCurrentTransaction(status: SpanStatus = 'ok'): void {
if (this.maxDurationTimer) {
clearTimeout(this.maxDurationTimer);
this.maxDurationTimer = null;
}
if (this.currentTransaction) {
this.currentTransaction.setStatus(status);
this.currentTransaction.finish();
this.currentTransaction = null;
}
}
destroy(): void {
if (this.originalPushState && typeof history !== 'undefined') {
history.pushState = this.originalPushState;
}
if (this.originalReplaceState && typeof history !== 'undefined') {
history.replaceState = this.originalReplaceState;
}
if (typeof window !== 'undefined') {
window.removeEventListener('popstate', this.handlePopState);
window.removeEventListener('hashchange', this.handleHashChange);
}
if (this.maxDurationTimer) {
clearTimeout(this.maxDurationTimer);
this.maxDurationTimer = null;
}
if (this.routeChangeTimer) {
clearTimeout(this.routeChangeTimer);
this.routeChangeTimer = null;
}
this.finishCurrentTransaction('aborted');
}
}
export default RouterPlugin;

View File

@ -54,10 +54,33 @@ export interface DeviceContext {
type?: string; type?: string;
} }
export interface TraceContext {
trace_id: string;
span_id: string;
parent_span_id?: string;
op?: string;
status?: string;
description?: string;
}
export interface BaggageFields {
trace_id?: string;
public_key?: string;
sample_rate?: string;
sampled?: string;
user_id?: string;
user_segment?: string;
release?: string;
environment?: string;
replay_id?: string;
[key: string]: string | undefined;
}
export interface Contexts { export interface Contexts {
browser?: BrowserContext; browser?: BrowserContext;
os?: OSContext; os?: OSContext;
device?: DeviceContext; device?: DeviceContext;
trace?: TraceContext;
} }
export interface Breadcrumb { export interface Breadcrumb {
@ -76,6 +99,7 @@ export interface BaseEvent {
release?: string; release?: string;
environment?: string; environment?: string;
user?: UserInfo; user?: UserInfo;
anonymousId?: string; // 匿名用户ID未登录用户
tags?: Record<string, string>; tags?: Record<string, string>;
extra?: Record<string, unknown>; extra?: Record<string, unknown>;
breadcrumbs?: Breadcrumb[]; breadcrumbs?: Breadcrumb[];
@ -129,7 +153,124 @@ export interface CountEvent extends BaseEvent {
ts_end: number; ts_end: number;
} }
export type SentryEvent = ErrorEvent | PerformanceEvent | NetworkEvent | BehaviorEvent | CountEvent; export type SpanStatus =
| 'ok'
| 'cancelled'
| 'unknown'
| 'unknown_error'
| 'invalid_argument'
| 'deadline_exceeded'
| 'not_found'
| 'already_exists'
| 'permission_denied'
| 'resource_exhausted'
| 'failed_precondition'
| 'aborted'
| 'out_of_range'
| 'unimplemented'
| 'internal_error'
| 'unavailable'
| 'data_loss'
| 'unauthenticated';
export interface SpanContext {
op?: string;
description?: string;
startTimestamp?: number;
status?: SpanStatus;
tags?: Record<string, string>;
data?: Record<string, unknown>;
traceId?: string;
spanId?: string;
parentSpanId?: string;
sampled?: boolean;
}
export interface SamplingContext extends TransactionContext {
parentSampled?: boolean;
}
export interface TransactionContext extends SpanContext {
name: string;
type?: string;
}
// Span JSON 输出类型
export interface SpanJSON {
trace_id: string;
span_id: string;
parent_span_id?: string;
op?: string;
description?: string;
status?: SpanStatus;
tags?: Record<string, string>;
data?: Record<string, unknown>;
start_timestamp: number;
timestamp: number;
}
// Transaction JSON 输出类型
export interface TransactionJSON extends SpanJSON {
transaction: string;
type?: string;
spans: SpanJSON[];
sampled?: boolean;
}
export interface Span {
readonly traceId: string;
readonly spanId: string;
readonly parentSpanId?: string;
op?: string;
description?: string;
status?: SpanStatus;
tags: Record<string, string>;
data: Record<string, unknown>;
readonly startTimestamp: number;
readonly endTimestamp?: number;
readonly transaction?: Transaction;
setTag(key: string, value: string): this;
setData(key: string, value: unknown): this;
setStatus(status: SpanStatus): this;
setHttpStatus(statusCode: number): this;
setStatusFromResponse(status: string | number | undefined, tagKey?: string): this;
startChild(context?: SpanContext): Span;
finish(endTimestamp?: number): void;
toTraceparent(): string;
toJSON(): SpanJSON;
}
// 使用 Omit 排除 Span 中需要 override 的属性Transaction 扩展其余属性
export interface Transaction extends Omit<Span, 'toJSON'> {
name: string;
type?: string;
spans: Span[];
sampled?: boolean;
maxSpans?: number;
toJSON(): TransactionJSON;
toEvent(): TransactionEvent;
}
export interface MeasurementValue {
value: number;
unit?: string;
rating?: 'good' | 'needs-improvement' | 'poor';
}
export interface TransactionEvent extends BaseEvent {
type: 'transaction';
transaction: string;
transaction_info?: {
source: string;
};
spans: Array<ReturnType<Span['toJSON']>>;
start_timestamp: number;
measurements?: Record<string, MeasurementValue>;
}
export type SentryEvent = ErrorEvent | PerformanceEvent | NetworkEvent | BehaviorEvent | CountEvent | TransactionEvent;
export interface DSNInfo { export interface DSNInfo {
protocol: string; protocol: string;
@ -155,6 +296,13 @@ export interface LightConfig {
enabled?: boolean; enabled?: boolean;
debug?: boolean; debug?: boolean;
sampleRate?: number; sampleRate?: number;
tracesSampleRate?: number; // Trace 采样率 0~1默认 0关闭
tracesSampler?: (context: SamplingContext) => number | boolean; // 动态采样函数
/**
* @deprecated 使 tracePropagationTargets
*/
tracingOrigins?: (string | RegExp)[]; // 追踪域名白名单(已废弃)
tracePropagationTargets?: (string | RegExp)[]; // trace header 传播目标
maxQueueSize?: number; maxQueueSize?: number;
flushInterval?: number; flushInterval?: number;
maxRetries?: number; maxRetries?: number;
@ -165,6 +313,7 @@ export interface LightConfig {
maxBreadcrumbs?: number; maxBreadcrumbs?: number;
beforeSend?: (event: SentryEvent) => SentryEvent | null; beforeSend?: (event: SentryEvent) => SentryEvent | null;
user?: UserInfo; user?: UserInfo;
trackAnonymousUsers?: boolean; // 是否启用匿名用户追踪,默认 true
plugins?: (LightPlugin | string)[]; plugins?: (LightPlugin | string)[];
contextLevel?: Partial<ContextLevelMap>; contextLevel?: Partial<ContextLevelMap>;
sampling?: SamplingConfig; sampling?: SamplingConfig;
@ -190,10 +339,16 @@ export interface LightClient {
captureMessage(message: string, level?: EventLevel): void; captureMessage(message: string, level?: EventLevel): void;
captureEvent(event: Partial<SentryEvent> & { type: string }): void; captureEvent(event: Partial<SentryEvent> & { type: string }): void;
setUser(user: UserInfo | null): void; setUser(user: UserInfo | null): void;
setAnonymousId(id: string): void; // 设置匿名用户ID
setTag(key: string, value: string): void; setTag(key: string, value: string): void;
setTags(tags: Record<string, string>): void; setTags(tags: Record<string, string>): void;
setExtra(key: string, value: unknown): void; setExtra(key: string, value: unknown): void;
addBreadcrumb(breadcrumb: Omit<Breadcrumb, 'timestamp'>): void; addBreadcrumb(breadcrumb: Omit<Breadcrumb, 'timestamp'>): void;
startTransaction(context: TransactionContext): Transaction | null;
getCurrentTransaction(): Transaction | null;
getCurrentSpan(): Span | null;
setSpan(span: Span | null): void;
startSpan(context: SpanContext): Span | null;
flush(): Promise<void>; flush(): Promise<void>;
disable(): void; disable(): void;
enable(): void; enable(): void;

99
src/utils/baggage.ts Normal file
View File

@ -0,0 +1,99 @@
import type { BaggageFields } from '../types';
const SENTRY_PREFIX = 'sentry-';
const MAX_BAGGAGE_LENGTH = 8192;
export function serializeBaggage(fields: BaggageFields): string {
const parts: string[] = [];
for (const [key, value] of Object.entries(fields)) {
if (value === undefined || value === null || value === '') continue;
const prefixedKey = key.startsWith(SENTRY_PREFIX) ? key : `${SENTRY_PREFIX}${key}`;
parts.push(`${prefixedKey}=${encodeURIComponent(String(value))}`);
}
let result = parts.join(',');
if (result.length > MAX_BAGGAGE_LENGTH) {
result = result.substring(0, MAX_BAGGAGE_LENGTH);
const lastComma = result.lastIndexOf(',');
if (lastComma > 0) {
result = result.substring(0, lastComma);
}
}
return result;
}
export function parseBaggage(header: string): BaggageFields {
const fields: BaggageFields = {};
if (!header) return fields;
const parts = header.split(',');
for (const part of parts) {
const eqIdx = part.indexOf('=');
if (eqIdx === -1) continue;
const rawKey = part.substring(0, eqIdx).trim();
const value = decodeURIComponent(part.substring(eqIdx + 1).trim());
let key = rawKey;
if (rawKey.startsWith(SENTRY_PREFIX)) {
key = rawKey.substring(SENTRY_PREFIX.length);
}
if (key) {
fields[key] = value;
}
}
return fields;
}
export function createBaggageFromTransaction(
traceId: string,
options?: {
dsnPublicKey?: string;
sampled?: boolean;
sampleRate?: number;
user?: { id?: string; segment?: string };
release?: string;
environment?: string;
}
): BaggageFields {
const fields: BaggageFields = {
trace_id: traceId,
};
if (options?.dsnPublicKey) {
fields.public_key = options.dsnPublicKey;
}
if (options?.sampled !== undefined) {
fields.sampled = options.sampled ? 'true' : 'false';
}
if (options?.sampleRate !== undefined) {
fields.sample_rate = String(options.sampleRate);
}
if (options?.user) {
if (options.user.id) {
fields.user_id = options.user.id;
}
if (options.user.segment) {
fields.user_segment = options.user.segment;
}
}
if (options?.release) {
fields.release = options.release;
}
if (options?.environment) {
fields.environment = options.environment;
}
return fields;
}

View File

@ -163,6 +163,11 @@ export function getReferrer(): string {
export function sanitizeUrl(url: string): string { export function sanitizeUrl(url: string): string {
if (!url) return url; if (!url) return url;
// 过滤 javascript: 伪协议防止 XSS
if (/^\s*javascript:/i.test(url)) {
return 'javascript:[filtered]';
}
try { try {
const urlObj = new URL(url); const urlObj = new URL(url);
const sensitiveParams = [ const sensitiveParams = [

70
src/utils/identity.ts Normal file
View File

@ -0,0 +1,70 @@
import { uuid } from './helper';
const ANONYMOUS_ID_KEY = 'light_sentry_aid';
/**
* ID
* localStorage ID
*/
export function getAnonymousId(): string | null {
// 浏览器环境检查
if (typeof localStorage === 'undefined') {
return null;
}
try {
let anonymousId = localStorage.getItem(ANONYMOUS_ID_KEY);
if (!anonymousId) {
anonymousId = uuid();
localStorage.setItem(ANONYMOUS_ID_KEY, anonymousId);
}
return anonymousId;
} catch {
// localStorage 不可用(如隐私模式、跨域限制等)
return null;
}
}
/**
* ID
* ID
*/
export function setAnonymousId(id: string): boolean {
if (typeof localStorage === 'undefined') {
return false;
}
try {
localStorage.setItem(ANONYMOUS_ID_KEY, id);
return true;
} catch {
return false;
}
}
/**
* ID
*
*/
export function clearAnonymousId(): boolean {
if (typeof localStorage === 'undefined') {
return false;
}
try {
localStorage.removeItem(ANONYMOUS_ID_KEY);
return true;
} catch {
return false;
}
}
/**
*
*/
export function isAnonymousTrackingEnabled(config: { trackAnonymousUsers?: boolean }): boolean {
// 默认为 true除非明确设置为 false
return config.trackAnonymousUsers !== false;
}

356
src/utils/tracing.ts Normal file
View File

@ -0,0 +1,356 @@
import type { Span as SpanInterface, SpanContext, Transaction as TransactionInterface, TransactionContext, SpanStatus, LightClient, BaggageFields } from '../types';
import { uuid, now } from './helper';
import { createBaggageFromTransaction, serializeBaggage } from './baggage';
function generateTraceId(): string {
return uuid().replace(/-/g, '');
}
function generateSpanId(): string {
return uuid().replace(/-/g, '').substring(0, 16);
}
export function httpStatusToSpanStatus(statusCode: number): SpanStatus {
if (statusCode >= 200 && statusCode < 300) return 'ok';
if (statusCode === 400) return 'invalid_argument';
if (statusCode === 401) return 'unauthenticated';
if (statusCode === 403) return 'permission_denied';
if (statusCode === 404) return 'not_found';
if (statusCode === 409) return 'already_exists';
if (statusCode === 429) return 'resource_exhausted';
if (statusCode >= 400 && statusCode < 500) return 'invalid_argument';
if (statusCode >= 500 && statusCode < 600) return 'internal_error';
return 'unknown_error';
}
export function dbStatusToSpanStatus(dbStatusCode?: string | number): SpanStatus {
if (dbStatusCode === undefined || dbStatusCode === null || dbStatusCode === '') {
return 'unknown_error';
}
const code = typeof dbStatusCode === 'string' ? parseInt(dbStatusCode, 10) : dbStatusCode;
if (code === 0 || code === 1) return 'ok';
if (code >= 10 && code < 20) return 'invalid_argument';
if (code >= 20 && code < 30) return 'not_found';
if (code >= 30 && code < 40) return 'already_exists';
if (code >= 50) return 'internal_error';
return 'unknown_error';
}
export function statusToSpanStatus(
status: string | number | undefined,
op: string | undefined
): SpanStatus {
if (status === undefined) return 'unknown_error';
if (typeof status === 'string') {
const lowerStatus = status.toLowerCase();
if (lowerStatus === 'ok' || lowerStatus === 'success') return 'ok';
if (lowerStatus === 'cancelled' || lowerStatus === 'canceled') return 'cancelled';
if (lowerStatus === 'deadline_exceeded') return 'deadline_exceeded';
if (lowerStatus === 'not_found') return 'not_found';
if (lowerStatus === 'already_exists') return 'already_exists';
if (lowerStatus === 'permission_denied') return 'permission_denied';
if (lowerStatus === 'resource_exhausted') return 'resource_exhausted';
if (lowerStatus === 'failed_precondition') return 'failed_precondition';
if (lowerStatus === 'invalid_argument') return 'invalid_argument';
if (lowerStatus === 'unauthenticated') return 'unauthenticated';
if (lowerStatus === 'internal_error') return 'internal_error';
const parsed = parseInt(status, 10);
if (!isNaN(parsed)) {
return statusToSpanStatus(parsed, op);
}
return 'unknown_error';
}
if (typeof status === 'number') {
if (status >= 200 && status < 600) {
return httpStatusToSpanStatus(status);
}
if (op?.startsWith('db')) {
return dbStatusToSpanStatus(status);
}
if (status === 0) return 'ok';
if (status > 0) return 'unknown_error';
}
return 'unknown_error';
}
export class Span implements SpanInterface {
readonly traceId: string;
readonly spanId: string;
readonly parentSpanId?: string;
op?: string;
description?: string;
status?: SpanStatus;
tags: Record<string, string> = {};
data: Record<string, unknown> = {};
readonly startTimestamp: number;
endTimestamp?: number;
transaction?: TransactionInterface;
protected _isFinished = false;
protected _client?: LightClient;
constructor(context: SpanContext, transaction?: TransactionInterface, client?: LightClient) {
this.traceId = context.traceId || generateTraceId();
this.spanId = context.spanId || generateSpanId();
this.parentSpanId = context.parentSpanId;
this.op = context.op;
this.description = context.description;
this.status = context.status;
this.tags = { ...context.tags };
this.data = { ...context.data };
this.startTimestamp = context.startTimestamp || now();
this.transaction = transaction;
this._client = client;
}
setTag(key: string, value: string): this {
this.tags[key] = value;
return this;
}
setData(key: string, value: unknown): this {
this.data[key] = value;
return this;
}
setStatus(status: SpanStatus): this {
this.status = status;
return this;
}
setHttpStatus(statusCode: number): this {
this.setStatus(httpStatusToSpanStatus(statusCode));
this.setTag('http.status_code', String(statusCode));
return this;
}
setStatusFromResponse(status: string | number | undefined, tagKey?: string): this {
const spanStatus = statusToSpanStatus(status, this.op);
this.setStatus(spanStatus);
if (tagKey && status !== undefined) {
this.setTag(tagKey, String(status));
}
return this;
}
startChild(context: SpanContext = {}): Span {
const tx = this.transaction;
const isSampled = tx?.sampled !== false;
const child = new Span(
{
...context,
traceId: this.traceId,
parentSpanId: this.spanId,
},
tx,
this._client
);
if (tx && isSampled) {
(tx as Transaction)._addSpan(child);
}
return child;
}
finish(endTimestamp?: number): void {
if (this._isFinished) return;
this._isFinished = true;
this.endTimestamp = endTimestamp || now();
if (this._client) {
const clientInternal = this._client as unknown as { tracingManager?: { onSpanFinished: (span: Span) => void } };
if (clientInternal.tracingManager?.onSpanFinished) {
clientInternal.tracingManager.onSpanFinished(this);
}
}
}
toTraceparent(): string {
const sampled = this.transaction?.sampled;
const sampledFlag = sampled === undefined ? '' : sampled ? '1' : '0';
return sampledFlag
? `${this.traceId}-${this.spanId}-${sampledFlag}`
: `${this.traceId}-${this.spanId}`;
}
toJSON() {
return {
trace_id: this.traceId,
span_id: this.spanId,
parent_span_id: this.parentSpanId,
op: this.op,
description: this.description,
status: this.status,
tags: Object.keys(this.tags).length > 0 ? { ...this.tags } : undefined,
data: Object.keys(this.data).length > 0 ? { ...this.data } : undefined,
start_timestamp: this.startTimestamp,
timestamp: this.endTimestamp || this.startTimestamp,
};
}
}
export class Transaction extends Span implements TransactionInterface {
name: string;
type?: string;
spans: Span[] = [];
sampled?: boolean;
maxSpans: number = 1000;
private _baggage?: BaggageFields;
private _dsnPublicKey?: string;
private _release?: string;
private _environment?: string;
private _droppedSpans: number = 0;
constructor(context: TransactionContext, client?: LightClient, dsnPublicKey?: string, release?: string, environment?: string, maxSpans?: number) {
super(context, undefined, client);
this.name = context.name;
this.type = context.type;
this.sampled = context.sampled;
this.transaction = this;
this._dsnPublicKey = dsnPublicKey;
this._release = release;
this._environment = environment;
if (maxSpans !== undefined) {
this.maxSpans = maxSpans;
}
}
_addSpan(span: Span): void {
if (this.spans.length >= this.maxSpans) {
this._droppedSpans++;
return;
}
this.spans.push(span);
}
private _ensureBaggage(): BaggageFields {
if (!this._baggage) {
this._baggage = createBaggageFromTransaction(
this.traceId,
{
dsnPublicKey: this._dsnPublicKey,
sampled: this.sampled,
release: this._release,
environment: this._environment,
}
);
}
return this._baggage;
}
setBaggage(key: string, value: string): this {
const baggage = this._ensureBaggage();
baggage[key] = value;
return this;
}
setUserBaggage(userId?: string, segment?: string): this {
const baggage = this._ensureBaggage();
if (userId !== undefined) {
baggage.user_id = userId;
} else {
delete baggage.user_id;
}
if (segment !== undefined) {
baggage.user_segment = segment;
} else {
delete baggage.user_segment;
}
return this;
}
toBaggage(): string | undefined {
const baggage = this._ensureBaggage();
return serializeBaggage(baggage);
}
getBaggage(): BaggageFields | undefined {
return this._baggage;
}
finish(endTimestamp?: number): void {
if (this._isFinished) return;
super.finish(endTimestamp);
if (this._client && this.sampled) {
const event = this.toEvent();
this._client.captureEvent(event);
}
if (this._client) {
const clientInternal = this._client as unknown as { tracingManager?: { onTransactionFinished: (tx: Transaction) => void } };
if (clientInternal.tracingManager?.onTransactionFinished) {
clientInternal.tracingManager.onTransactionFinished(this);
}
}
}
private _getSource(): string {
switch (this.type) {
case 'pageload':
return 'url';
case 'navigation':
return 'route';
case 'user-interaction':
return 'ui';
default:
return 'custom';
}
}
toEvent() {
const json = this.toJSON();
const data = { ...json.data } as Record<string, unknown>;
const measurements = data.measurements as Record<string, { value: number; unit?: string; rating?: 'good' | 'needs-improvement' | 'poor' }> | undefined;
delete data.measurements;
return {
type: 'transaction' as const,
transaction: this.name,
transaction_info: {
source: this._getSource(),
},
level: 'info' as const,
timestamp: json.timestamp,
start_timestamp: json.start_timestamp,
spans: this.spans.map((s) => s.toJSON()),
contexts: {
trace: {
trace_id: this.traceId,
span_id: this.spanId,
parent_span_id: this.parentSpanId,
op: this.op,
status: this.status,
},
},
tags: json.tags,
extra: Object.keys(data).length > 0 ? data : undefined,
measurements,
platform: 'javascript',
};
}
toJSON() {
const base = super.toJSON();
return {
...base,
transaction: this.name,
type: this.type,
spans: this.spans.map((s) => s.toJSON()),
sampled: this.sampled,
};
}
}
export { generateTraceId, generateSpanId };

View File

@ -245,4 +245,256 @@ describe('ConfigManager', () => {
expect(result.context_id).toBe('test_context_id'); expect(result.context_id).toBe('test_context_id');
}); });
}); });
describe('anonymousId', () => {
beforeEach(() => {
localStorage.clear();
});
it('should add anonymousId when user.id is not set and tracking is enabled', () => {
configManager = new ConfigManager({
dsn: 'https://proj_abc123@log.example.com/1001',
trackAnonymousUsers: true,
});
const event: SentryEvent = {
type: 'error',
level: 'error',
timestamp: Date.now(),
} as ErrorEvent;
const result = configManager.applyToEvent(event);
expect((result as any).anonymousId).toBeTruthy();
expect(typeof (result as any).anonymousId).toBe('string');
});
it('should not add anonymousId when user.id is set', () => {
configManager = new ConfigManager({
dsn: 'https://proj_abc123@log.example.com/1001',
user: { id: 'user-123' },
trackAnonymousUsers: true,
});
const event: SentryEvent = {
type: 'error',
level: 'error',
timestamp: Date.now(),
} as ErrorEvent;
const result = configManager.applyToEvent(event);
expect((result as any).anonymousId).toBeUndefined();
});
it('should not add anonymousId when trackAnonymousUsers is false', () => {
configManager = new ConfigManager({
dsn: 'https://proj_abc123@log.example.com/1001',
trackAnonymousUsers: false,
});
const event: SentryEvent = {
type: 'error',
level: 'error',
timestamp: Date.now(),
} as ErrorEvent;
const result = configManager.applyToEvent(event);
expect((result as any).anonymousId).toBeUndefined();
});
it('should not add anonymousId when event has user.id', () => {
configManager = new ConfigManager({
dsn: 'https://proj_abc123@log.example.com/1001',
trackAnonymousUsers: true,
});
const event: SentryEvent = {
type: 'error',
level: 'error',
timestamp: Date.now(),
user: { id: 'event-user-id' },
} as ErrorEvent;
const result = configManager.applyToEvent(event);
expect((result as any).anonymousId).toBeUndefined();
});
});
describe('contextLevel', () => {
beforeEach(() => {
configManager = new ConfigManager({
dsn: 'https://proj_abc123@log.example.com/1001',
contextLevel: {
error: { maxStackFrames: 5, maxBreadcrumbs: 10 },
info: { maxStackFrames: 0, maxBreadcrumbs: 0 },
warning: { maxStackFrames: 3, maxBreadcrumbs: 5 },
},
});
});
it('should truncate stack frames for error level', () => {
const frames = Array.from({ length: 10 }, (_, i) => ({
filename: `file${i}.js`,
function: `fn${i}`,
lineno: i * 10,
colno: i,
}));
const event: SentryEvent = {
type: 'error',
level: 'error',
timestamp: Date.now(),
exception: {
type: 'Error',
value: 'test',
stacktrace: { frames },
},
} as ErrorEvent;
const result = configManager.applyToEvent(event);
expect((result as ErrorEvent).exception?.stacktrace?.frames).toHaveLength(5);
});
it('should remove exception entirely for info level (maxStackFrames=0)', () => {
const frames = [{ filename: 'file.js', function: 'fn', lineno: 1, colno: 1 }];
const event: SentryEvent = {
type: 'error',
level: 'info',
timestamp: Date.now(),
exception: {
type: 'Error',
value: 'test',
stacktrace: { frames },
},
} as ErrorEvent;
const result = configManager.applyToEvent(event);
expect((result as ErrorEvent).exception).toBeUndefined();
});
it('should truncate breadcrumbs for error level', () => {
const breadcrumbs = Array.from({ length: 20 }, (_, i) => ({
type: 'default',
message: `breadcrumb ${i}`,
timestamp: Date.now() + i,
}));
const event: SentryEvent = {
type: 'error',
level: 'error',
timestamp: Date.now(),
breadcrumbs,
} as ErrorEvent;
const result = configManager.applyToEvent(event);
expect(result.breadcrumbs).toHaveLength(10);
});
it('should remove breadcrumbs for info level (maxBreadcrumbs=0)', () => {
const breadcrumbs = [{ type: 'default', message: 'test', timestamp: Date.now() }];
const event: SentryEvent = {
type: 'error',
level: 'info',
timestamp: Date.now(),
breadcrumbs,
} as ErrorEvent;
const result = configManager.applyToEvent(event);
expect(result.breadcrumbs).toBeUndefined();
});
it('should keep last N breadcrumbs', () => {
const breadcrumbs = Array.from({ length: 20 }, (_, i) => ({
type: 'default',
message: `breadcrumb ${i}`,
timestamp: Date.now() + i,
}));
const event: SentryEvent = {
type: 'error',
level: 'warning',
timestamp: Date.now(),
breadcrumbs,
} as ErrorEvent;
const result = configManager.applyToEvent(event);
expect(result.breadcrumbs).toHaveLength(5);
// Should keep last 5
expect(result.breadcrumbs?.[0].message).toBe('breadcrumb 15');
expect(result.breadcrumbs?.[4].message).toBe('breadcrumb 19');
});
it('should not modify original event', () => {
const frames = Array.from({ length: 10 }, (_, i) => ({
filename: `file${i}.js`,
function: `fn${i}`,
lineno: i * 10,
colno: i,
}));
const event: ErrorEvent = {
type: 'error',
level: 'error',
message: 'test',
timestamp: Date.now(),
exception: {
type: 'Error',
value: 'test',
stacktrace: { frames },
},
};
configManager.applyToEvent(event);
// Original event should not be modified
expect(event.exception?.stacktrace?.frames).toHaveLength(10);
});
it('should handle event without exception', () => {
const event: SentryEvent = {
type: 'error',
level: 'error',
timestamp: Date.now(),
} as ErrorEvent;
const result = configManager.applyToEvent(event);
expect(result).toBeDefined();
});
it('should handle event without breadcrumbs', () => {
const event: SentryEvent = {
type: 'error',
level: 'error',
timestamp: Date.now(),
} as ErrorEvent;
const result = configManager.applyToEvent(event);
expect(result).toBeDefined();
});
});
describe('setUser', () => {
beforeEach(() => {
configManager = new ConfigManager({
dsn: 'https://proj_abc123@log.example.com/1001',
user: { id: 'existing-user' },
});
});
it('should set user info', () => {
configManager.setUser({ id: 'new-user', username: 'test' });
expect(configManager.get('user')).toEqual({ id: 'new-user', username: 'test' });
});
it('should clear user info when null is passed', () => {
configManager.setUser(null);
expect(configManager.get('user')).toBeUndefined();
});
});
describe('setTag / setTags / setExtra', () => {
beforeEach(() => {
configManager = new ConfigManager({
dsn: 'https://proj_abc123@log.example.com/1001',
});
});
it('should set tag when no tags exist', () => {
configManager.setTag('key1', 'value1');
expect(configManager.get('tags')).toEqual({ key1: 'value1' });
});
it('should merge tags', () => {
configManager.setTag('key1', 'value1');
configManager.setTags({ key2: 'value2', key3: 'value3' });
expect(configManager.get('tags')).toEqual({
key1: 'value1',
key2: 'value2',
key3: 'value3',
});
});
it('should set extra when no extra exists', () => {
configManager.setExtra('key1', 'value1');
expect(configManager.get('extra')).toEqual({ key1: 'value1' });
});
});
}); });

189
tests/EventBus.test.ts Normal file
View File

@ -0,0 +1,189 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { EventBus } from '../src/core/EventBus';
describe('EventBus', () => {
let consoleSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('on / emit', () => {
it('should register a handler and call it on emit', () => {
const bus = new EventBus();
const handler = vi.fn();
bus.on('test-event', handler);
bus.emit('test-event', 'data1', 'data2');
expect(handler).toHaveBeenCalledTimes(1);
expect(handler).toHaveBeenCalledWith('data1', 'data2');
});
it('should call multiple handlers on same event', () => {
const bus = new EventBus();
const handler1 = vi.fn();
const handler2 = vi.fn();
bus.on('test-event', handler1);
bus.on('test-event', handler2);
bus.emit('test-event');
expect(handler1).toHaveBeenCalledTimes(1);
expect(handler2).toHaveBeenCalledTimes(1);
});
it('should not call handlers for different events', () => {
const bus = new EventBus();
const handler = vi.fn();
bus.on('event-a', handler);
bus.emit('event-b');
expect(handler).not.toHaveBeenCalled();
});
it('should handle emit on non-existent event', () => {
const bus = new EventBus();
expect(() => bus.emit('non-existent', 'data')).not.toThrow();
});
it('should pass multiple arguments to handler', () => {
const bus = new EventBus();
const handler = vi.fn();
bus.on('multi-arg', handler);
bus.emit('multi-arg', 1, 'two', { three: 3 }, [4, 5]);
expect(handler).toHaveBeenCalledWith(1, 'two', { three: 3 }, [4, 5]);
});
it('should not throw if a handler throws', () => {
const bus = new EventBus();
const handler1 = vi.fn(() => {
throw new Error('Handler error');
});
const handler2 = vi.fn();
bus.on('error-event', handler1);
bus.on('error-event', handler2);
expect(() => bus.emit('error-event')).not.toThrow();
expect(handler2).toHaveBeenCalledTimes(1);
expect(consoleSpy).toHaveBeenCalled();
});
});
describe('off', () => {
it('should remove a registered handler', () => {
const bus = new EventBus();
const handler = vi.fn();
bus.on('test-event', handler);
bus.off('test-event', handler);
bus.emit('test-event');
expect(handler).not.toHaveBeenCalled();
});
it('should not remove other handlers', () => {
const bus = new EventBus();
const handler1 = vi.fn();
const handler2 = vi.fn();
bus.on('test-event', handler1);
bus.on('test-event', handler2);
bus.off('test-event', handler1);
bus.emit('test-event');
expect(handler1).not.toHaveBeenCalled();
expect(handler2).toHaveBeenCalledTimes(1);
});
it('should handle off for non-existent event', () => {
const bus = new EventBus();
const handler = vi.fn();
expect(() => bus.off('non-existent', handler)).not.toThrow();
});
it('should handle off for non-existent handler', () => {
const bus = new EventBus();
const handler1 = vi.fn();
const handler2 = vi.fn();
bus.on('test-event', handler1);
bus.off('test-event', handler2);
bus.emit('test-event');
expect(handler1).toHaveBeenCalledTimes(1);
});
it('should only remove one instance if handler is registered multiple times', () => {
const bus = new EventBus();
const handler = vi.fn();
bus.on('test-event', handler);
bus.on('test-event', handler);
bus.off('test-event', handler);
bus.emit('test-event');
expect(handler).toHaveBeenCalledTimes(1);
});
});
describe('once', () => {
it('should call handler only once', () => {
const bus = new EventBus();
const handler = vi.fn();
bus.once('once-event', handler);
bus.emit('once-event');
bus.emit('once-event');
expect(handler).toHaveBeenCalledTimes(1);
});
it('should pass arguments to once handler', () => {
const bus = new EventBus();
const handler = vi.fn();
bus.once('once-event', handler);
bus.emit('once-event', 'arg1', 'arg2');
expect(handler).toHaveBeenCalledWith('arg1', 'arg2');
});
it('should work with on and once mixed', () => {
const bus = new EventBus();
const onHandler = vi.fn();
const onceHandler = vi.fn();
bus.on('mixed-event', onHandler);
bus.once('mixed-event', onceHandler);
bus.emit('mixed-event');
bus.emit('mixed-event');
expect(onHandler).toHaveBeenCalledTimes(2);
expect(onceHandler).toHaveBeenCalledTimes(1);
});
it('should not throw if once handler throws', () => {
const bus = new EventBus();
const handler = vi.fn(() => {
throw new Error('Once handler error');
});
bus.once('once-error', handler);
expect(() => bus.emit('once-error')).not.toThrow();
expect(consoleSpy).toHaveBeenCalled();
});
});
describe('destroy', () => {
it('should clear all handlers', () => {
const bus = new EventBus();
const handler1 = vi.fn();
const handler2 = vi.fn();
bus.on('event-a', handler1);
bus.on('event-b', handler2);
bus.destroy();
bus.emit('event-a');
bus.emit('event-b');
expect(handler1).not.toHaveBeenCalled();
expect(handler2).not.toHaveBeenCalled();
});
it('should handle destroy on empty bus', () => {
const bus = new EventBus();
expect(() => bus.destroy()).not.toThrow();
});
it('should work with once handlers after destroy', () => {
const bus = new EventBus();
const handler = vi.fn();
bus.once('event', handler);
bus.destroy();
bus.emit('event');
expect(handler).not.toHaveBeenCalled();
});
});
});

383
tests/EventQueue.test.ts Normal file
View File

@ -0,0 +1,383 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { EventQueue } from '../src/core/EventQueue';
import { EventBus } from '../src/core/EventBus';
import type { SentryEvent, ErrorEvent } from '../src/types';
describe('EventQueue', () => {
let eventQueue: EventQueue;
let eventBus: EventBus;
let flushCallback: ReturnType<typeof vi.fn>;
let syncFlushCallback: ReturnType<typeof vi.fn>;
beforeEach(() => {
eventBus = new EventBus();
flushCallback = vi.fn().mockResolvedValue(undefined);
syncFlushCallback = vi.fn();
eventQueue = new EventQueue(100, 5000, eventBus, flushCallback, syncFlushCallback);
});
afterEach(() => {
eventQueue.destroy();
vi.clearAllMocks();
});
describe('enqueue', () => {
it('should add event to queue', () => {
const event = createErrorEvent('Test error');
eventQueue.enqueue(event);
expect(eventQueue.size()).toBe(1);
});
it('should emit event when enqueueing', () => {
const event = createErrorEvent('Test error');
const emitSpy = vi.spyOn(eventBus, 'emit');
eventQueue.enqueue(event);
expect(emitSpy).toHaveBeenCalledWith('event', event);
});
it('should trigger flush when queue is full', () => {
const eventQueueSmall = new EventQueue(2, 5000, eventBus, flushCallback, syncFlushCallback);
eventQueueSmall.enqueue(createErrorEvent('error 1'));
eventQueueSmall.enqueue(createErrorEvent('error 2'));
// flushCallback should be called due to maxSize reached
expect(flushCallback).toHaveBeenCalled();
eventQueueSmall.destroy();
});
it('should skip deduplicated events', () => {
const event1 = createErrorEvent('Duplicate error', 'fingerprint1');
const event2 = createErrorEvent('Duplicate error', 'fingerprint1');
const event3 = createErrorEvent('Duplicate error', 'fingerprint1');
const event4 = createErrorEvent('Duplicate error', 'fingerprint1'); // count >= 3, should be deduped
eventQueue.enqueue(event1);
eventQueue.enqueue(event2);
eventQueue.enqueue(event3);
eventQueue.enqueue(event4);
// First 3 events are allowed, 4th is deduped
expect(eventQueue.size()).toBe(3);
});
it('should not skip non-duplicate events', () => {
const event1 = createErrorEvent('Error 1', 'fp1');
const event2 = createErrorEvent('Error 2', 'fp2');
eventQueue.enqueue(event1);
eventQueue.enqueue(event2);
expect(eventQueue.size()).toBe(2);
});
});
describe('flush', () => {
it('should call flush callback with events', async () => {
const event1 = createErrorEvent('Error 1');
const event2 = createErrorEvent('Error 2');
eventQueue.enqueue(event1);
eventQueue.enqueue(event2);
await eventQueue.flush();
expect(flushCallback).toHaveBeenCalledWith(expect.arrayContaining([
expect.objectContaining({ message: 'Error 1' }),
expect.objectContaining({ message: 'Error 2' }),
]));
});
it('should not flush when queue is empty', async () => {
await eventQueue.flush();
expect(flushCallback).not.toHaveBeenCalled();
});
it('should clear queue after successful flush', async () => {
const event = createErrorEvent('Test error');
eventQueue.enqueue(event);
expect(eventQueue.size()).toBe(1);
await eventQueue.flush();
expect(eventQueue.size()).toBe(0);
});
it('should return events to queue on flush failure', async () => {
flushCallback.mockRejectedValueOnce(new Error('Network error'));
const event = createErrorEvent('Test error');
eventQueue.enqueue(event);
await expect(eventQueue.flush()).rejects.toThrow('Network error');
// Events should be returned to queue
expect(eventQueue.size()).toBe(1);
});
it('should prevent concurrent flush reentry', async () => {
flushCallback.mockImplementation(async () => {
await new Promise(resolve => setTimeout(resolve, 50));
});
const event = createErrorEvent('Test error');
eventQueue.enqueue(event);
// Start first flush
const flushPromise = eventQueue.flush();
// Try second flush immediately
await eventQueue.flush();
await flushPromise;
// flushCallback should only be called once
expect(flushCallback).toHaveBeenCalledTimes(1);
});
});
describe('flushSync', () => {
it('should call sync flush callback', () => {
const event = createErrorEvent('Sync error');
eventQueue.enqueue(event);
eventQueue.flushSync();
expect(syncFlushCallback).toHaveBeenCalledWith(expect.arrayContaining([
expect.objectContaining({ message: 'Sync error' }),
]));
});
it('should return events to queue on sync flush failure', () => {
syncFlushCallback.mockImplementationOnce(() => {
throw new Error('Sync error');
});
const event = createErrorEvent('Test error');
eventQueue.enqueue(event);
eventQueue.flushSync();
// Events should be returned to queue
expect(eventQueue.size()).toBe(1);
});
});
describe('destroy', () => {
it('should clear all data', () => {
const event = createErrorEvent('Test error');
eventQueue.enqueue(event);
expect(eventQueue.size()).toBe(1);
eventQueue.destroy();
expect(eventQueue.size()).toBe(0);
});
});
describe('deduplication', () => {
it('should generate dedup key from fingerprint first', () => {
const event = createErrorEvent('Test error', 'fp1');
eventQueue.enqueue(event);
expect(eventQueue.size()).toBe(1);
});
it('should generate dedup key from message if no fingerprint', () => {
const event1 = createErrorEvent('Same message');
const event2 = createErrorEvent('Same message');
const event3 = createErrorEvent('Same message');
const event4 = createErrorEvent('Same message');
eventQueue.enqueue(event1);
eventQueue.enqueue(event2);
eventQueue.enqueue(event3);
eventQueue.enqueue(event4);
expect(eventQueue.size()).toBe(3);
});
it('should reset dedup entry after expiry', async () => {
const event1 = createErrorEvent('Test', 'fp-expire');
const event2 = createErrorEvent('Test', 'fp-expire');
const event3 = createErrorEvent('Test', 'fp-expire');
eventQueue.enqueue(event1);
eventQueue.enqueue(event2);
eventQueue.enqueue(event3);
expect(eventQueue.size()).toBe(3);
// 4th event should be deduped
const event4 = createErrorEvent('Test', 'fp-expire');
eventQueue.enqueue(event4);
expect(eventQueue.size()).toBe(3);
});
});
describe('count events', () => {
it('should build count events for deduped errors', async () => {
// Enqueue 3 events to reach threshold
for (let i = 0; i < 3; i++) {
eventQueue.enqueue(createErrorEvent('Dedup error', 'count-fp'));
}
// 4th event is deduped and goes to pendingCounts
eventQueue.enqueue(createErrorEvent('Dedup error', 'count-fp'));
eventQueue.enqueue(createErrorEvent('Dedup error', 'count-fp'));
expect(eventQueue.size()).toBe(3);
// Flush should include count events
await eventQueue.flush();
expect(flushCallback).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({ type: 'count', fingerprint: 'count-fp', count: 2 }),
])
);
});
it('should flush pending counts even when regular queue is empty', async () => {
for (let i = 0; i < 3; i++) {
eventQueue.enqueue(createErrorEvent('Dedup error', 'count-only-fp'));
}
// Add some deduped events (keep below infinite loop threshold of 10)
for (let i = 0; i < 5; i++) {
eventQueue.enqueue(createErrorEvent('Dedup error', 'count-only-fp'));
}
// Flush first to clear regular queue
await eventQueue.flush();
flushCallback.mockClear();
// Enqueue more deduped events
for (let i = 0; i < 3; i++) {
eventQueue.enqueue(createErrorEvent('Dedup error 2', 'count-only-fp-2'));
}
for (let i = 0; i < 2; i++) {
eventQueue.enqueue(createErrorEvent('Dedup error 2', 'count-only-fp-2'));
}
// Flush again - should have count events even with empty regular queue
await eventQueue.flush();
const lastCall = flushCallback.mock.calls[flushCallback.mock.calls.length - 1];
const countEvents = lastCall[0].filter((e: any) => e.type === 'count');
expect(countEvents.length).toBeGreaterThan(0);
});
});
describe('infinite loop detection', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should pause queue when too many errors in 1 second', () => {
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
for (let i = 0; i < 11; i++) {
eventQueue.enqueue(createErrorEvent('Infinite loop test', 'infinite-fp'));
}
// Should have paused
expect(consoleWarnSpy).toHaveBeenCalled();
// After pause, no more events should be added
const sizeAfterLoop = eventQueue.size();
eventQueue.enqueue(createErrorEvent('After pause', 'infinite-fp'));
expect(eventQueue.size()).toBe(sizeAfterLoop);
consoleWarnSpy.mockRestore();
});
it('should emit error event on infinite loop detection', () => {
const emitSpy = vi.spyOn(eventBus, 'emit');
for (let i = 0; i < 11; i++) {
eventQueue.enqueue(createErrorEvent('Infinite loop', 'loop-fp'));
}
expect(emitSpy).toHaveBeenCalledWith('error', expect.any(Error));
});
it('should resume after pause duration', () => {
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
for (let i = 0; i < 11; i++) {
eventQueue.enqueue(createErrorEvent('Resume test', 'resume-fp'));
}
const sizeBeforeResume = eventQueue.size();
// Advance time past the 60s pause
vi.advanceTimersByTime(61000);
// Should resume accepting events
eventQueue.enqueue(createErrorEvent('After resume', 'different-fp'));
expect(eventQueue.size()).toBe(sizeBeforeResume + 1);
consoleWarnSpy.mockRestore();
consoleInfoSpy.mockRestore();
});
});
describe('non-error events', () => {
it('should enqueue non-error events without dedup', () => {
const transactionEvent: SentryEvent = {
type: 'transaction',
level: 'info',
transaction: 'test-tx',
timestamp: Date.now(),
} as any;
eventQueue.enqueue(transactionEvent);
eventQueue.enqueue(transactionEvent);
expect(eventQueue.size()).toBe(2);
});
});
describe('start/stop timer', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should start timer and flush on interval when queue has events', async () => {
const eq = new EventQueue(100, 1000, eventBus, flushCallback, syncFlushCallback);
eq.start();
eq.enqueue(createErrorEvent('Interval test'));
// flushCallback should not be called yet
expect(flushCallback).not.toHaveBeenCalled();
// Advance timer past interval
vi.advanceTimersByTime(1500);
// Wait for the flush promise to resolve
await vi.runOnlyPendingTimersAsync();
expect(flushCallback).toHaveBeenCalled();
eq.destroy();
});
it('should not flush on interval when queue is empty', async () => {
const eq = new EventQueue(100, 1000, eventBus, flushCallback, syncFlushCallback);
eq.start();
vi.advanceTimersByTime(1500);
await vi.runOnlyPendingTimersAsync();
expect(flushCallback).not.toHaveBeenCalled();
eq.destroy();
});
});
});
function createErrorEvent(message: string, fingerprint?: string): ErrorEvent {
return {
type: 'error',
level: 'error',
message,
timestamp: Date.now(),
fingerprint,
};
}

472
tests/PluginManager.test.ts Normal file
View File

@ -0,0 +1,472 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { PluginManager } from '../src/core/PluginManager';
import type { LightPlugin, LightClient, SentryEvent } from '../src/types';
describe('PluginManager', () => {
let mockClient: LightClient;
let consoleSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
mockClient = {
config: { dsn: 'https://abc@sentry.io/123' },
dsn: { protocol: 'https', publicKey: 'abc', host: 'sentry.io', projectId: '123' },
on: vi.fn(),
off: vi.fn(),
emit: vi.fn(),
captureException: vi.fn(),
captureMessage: vi.fn(),
captureEvent: vi.fn(),
setUser: vi.fn(),
setAnonymousId: vi.fn(),
setTag: vi.fn(),
setTags: vi.fn(),
setExtra: vi.fn(),
addBreadcrumb: vi.fn(),
startTransaction: vi.fn(),
getCurrentTransaction: vi.fn(),
getCurrentSpan: vi.fn(),
setSpan: vi.fn(),
startSpan: vi.fn(),
flush: vi.fn(),
disable: vi.fn(),
enable: vi.fn(),
use: vi.fn(),
init: vi.fn(),
destroy: vi.fn(),
} as unknown as LightClient;
consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
});
describe('constructor', () => {
it('should create a PluginManager instance', () => {
const manager = new PluginManager(mockClient);
expect(manager).toBeDefined();
});
});
describe('add', () => {
it('should add a plugin', () => {
const manager = new PluginManager(mockClient);
const plugin: LightPlugin = {
name: 'test-plugin',
version: '1.0.0',
setup: vi.fn(),
};
manager.add(plugin);
expect(manager.has('test-plugin')).toBe(true);
});
it('should not add duplicate plugin', () => {
const manager = new PluginManager(mockClient);
const setup1 = vi.fn();
const setup2 = vi.fn();
const plugin1: LightPlugin = {
name: 'test-plugin',
version: '1.0.0',
setup: setup1,
};
const plugin2: LightPlugin = {
name: 'test-plugin',
version: '2.0.0',
setup: setup2,
};
manager.add(plugin1);
manager.add(plugin2);
expect(manager.has('test-plugin')).toBe(true);
expect(setup1).toHaveBeenCalledWith(mockClient);
expect(setup2).not.toHaveBeenCalled();
});
it('should call plugin setup with client', () => {
const manager = new PluginManager(mockClient);
const setupFn = vi.fn();
const plugin: LightPlugin = {
name: 'setup-test',
version: '1.0.0',
setup: setupFn,
};
manager.add(plugin);
expect(setupFn).toHaveBeenCalledWith(mockClient);
});
it('should not throw if setup throws', () => {
const manager = new PluginManager(mockClient);
const plugin: LightPlugin = {
name: 'error-plugin',
version: '1.0.0',
setup: () => {
throw new Error('Setup error');
},
};
expect(() => manager.add(plugin)).not.toThrow();
expect(consoleSpy).toHaveBeenCalled();
});
});
describe('remove', () => {
it('should remove a plugin', () => {
const manager = new PluginManager(mockClient);
const plugin: LightPlugin = {
name: 'removable',
version: '1.0.0',
setup: vi.fn(),
};
manager.add(plugin);
expect(manager.has('removable')).toBe(true);
manager.remove('removable');
expect(manager.has('removable')).toBe(false);
});
it('should call plugin destroy when removing', () => {
const manager = new PluginManager(mockClient);
const destroyFn = vi.fn();
const plugin: LightPlugin = {
name: 'destroyable',
version: '1.0.0',
setup: vi.fn(),
destroy: destroyFn,
};
manager.add(plugin);
manager.remove('destroyable');
expect(destroyFn).toHaveBeenCalled();
});
it('should not throw if destroy throws', () => {
const manager = new PluginManager(mockClient);
const plugin: LightPlugin = {
name: 'destroy-error',
version: '1.0.0',
setup: vi.fn(),
destroy: () => {
throw new Error('Destroy error');
},
};
manager.add(plugin);
expect(() => manager.remove('destroy-error')).not.toThrow();
expect(consoleSpy).toHaveBeenCalled();
});
it('should do nothing if plugin does not exist', () => {
const manager = new PluginManager(mockClient);
manager.remove('non-existent');
expect(consoleSpy).not.toHaveBeenCalled();
});
it('should do nothing if plugin has no destroy method', () => {
const manager = new PluginManager(mockClient);
const plugin: LightPlugin = {
name: 'no-destroy',
version: '1.0.0',
setup: vi.fn(),
};
manager.add(plugin);
expect(() => manager.remove('no-destroy')).not.toThrow();
});
});
describe('get', () => {
it('should return plugin by name', () => {
const manager = new PluginManager(mockClient);
const plugin: LightPlugin = {
name: 'gettable',
version: '1.0.0',
setup: vi.fn(),
};
manager.add(plugin);
const retrieved = manager.get('gettable');
expect(retrieved).toBe(plugin);
});
it('should return undefined for non-existent plugin', () => {
const manager = new PluginManager(mockClient);
const retrieved = manager.get('non-existent');
expect(retrieved).toBeUndefined();
});
});
describe('has', () => {
it('should return true for added plugin', () => {
const manager = new PluginManager(mockClient);
const plugin: LightPlugin = {
name: 'checkable',
version: '1.0.0',
setup: vi.fn(),
};
manager.add(plugin);
expect(manager.has('checkable')).toBe(true);
});
it('should return false for non-existent plugin', () => {
const manager = new PluginManager(mockClient);
expect(manager.has('non-existent')).toBe(false);
});
});
describe('applyBeforeReport', () => {
it('should pass event through all plugins', () => {
const manager = new PluginManager(mockClient);
const plugin1: LightPlugin = {
name: 'before-1',
version: '1.0.0',
setup: vi.fn(),
beforeReport: (event) => ({ ...event, extra: { ...event.extra, from1: true } }),
};
const plugin2: LightPlugin = {
name: 'before-2',
version: '1.0.0',
setup: vi.fn(),
beforeReport: (event) => ({ ...event, extra: { ...event.extra, from2: true } }),
};
manager.add(plugin1);
manager.add(plugin2);
const event = { type: 'error', level: 'error', timestamp: Date.now() } as SentryEvent;
const result = manager.applyBeforeReport(event);
expect(result.extra).toEqual({ from1: true, from2: true });
});
it('should return null if any plugin filters the event', () => {
const manager = new PluginManager(mockClient);
const plugin1: LightPlugin = {
name: 'filter-1',
version: '1.0.0',
setup: vi.fn(),
beforeReport: (event) => event,
};
const plugin2: LightPlugin = {
name: 'filter-2',
version: '1.0.0',
setup: vi.fn(),
beforeReport: () => null,
};
manager.add(plugin1);
manager.add(plugin2);
const event = { type: 'error', level: 'error', timestamp: Date.now() } as SentryEvent;
const result = manager.applyBeforeReport(event);
expect(result).toBeNull();
});
it('should continue if plugin beforeReport throws', () => {
const manager = new PluginManager(mockClient);
const plugin1: LightPlugin = {
name: 'error-before',
version: '1.0.0',
setup: vi.fn(),
beforeReport: () => {
throw new Error('BeforeReport error');
},
};
const plugin2: LightPlugin = {
name: 'ok-after',
version: '1.0.0',
setup: vi.fn(),
beforeReport: (event) => ({ ...event, extra: { passed: true } }),
};
manager.add(plugin1);
manager.add(plugin2);
const event = { type: 'error', level: 'error', timestamp: Date.now() } as SentryEvent;
const result = manager.applyBeforeReport(event);
expect(result?.extra).toEqual({ passed: true });
expect(consoleSpy).toHaveBeenCalled();
});
it('should skip plugin without beforeReport', () => {
const manager = new PluginManager(mockClient);
const plugin1: LightPlugin = {
name: 'no-before',
version: '1.0.0',
setup: vi.fn(),
};
const plugin2: LightPlugin = {
name: 'modify-after',
version: '1.0.0',
setup: vi.fn(),
beforeReport: (event) => ({ ...event, extra: { modified: true } }),
};
manager.add(plugin1);
manager.add(plugin2);
const event = { type: 'error', level: 'error', timestamp: Date.now() } as SentryEvent;
const result = manager.applyBeforeReport(event);
expect(result?.extra).toEqual({ modified: true });
});
it('should return event when no plugins', () => {
const manager = new PluginManager(mockClient);
const event = { type: 'error', level: 'error', timestamp: Date.now() } as SentryEvent;
const result = manager.applyBeforeReport(event);
expect(result).toBe(event);
});
});
describe('applyAfterReport', () => {
it('should call afterReport on all plugins', () => {
const manager = new PluginManager(mockClient);
const after1 = vi.fn();
const after2 = vi.fn();
const plugin1: LightPlugin = {
name: 'after-1',
version: '1.0.0',
setup: vi.fn(),
afterReport: after1,
};
const plugin2: LightPlugin = {
name: 'after-2',
version: '1.0.0',
setup: vi.fn(),
afterReport: after2,
};
manager.add(plugin1);
manager.add(plugin2);
const event = { type: 'error', level: 'error', timestamp: Date.now() } as SentryEvent;
manager.applyAfterReport(event);
expect(after1).toHaveBeenCalledWith(event);
expect(after2).toHaveBeenCalledWith(event);
});
it('should continue if afterReport throws', () => {
const manager = new PluginManager(mockClient);
const plugin1: LightPlugin = {
name: 'error-after',
version: '1.0.0',
setup: vi.fn(),
afterReport: () => {
throw new Error('AfterReport error');
},
};
const plugin2: LightPlugin = {
name: 'ok-after',
version: '1.0.0',
setup: vi.fn(),
afterReport: vi.fn(),
};
manager.add(plugin1);
manager.add(plugin2);
const event = { type: 'error', level: 'error', timestamp: Date.now() } as SentryEvent;
expect(() => manager.applyAfterReport(event)).not.toThrow();
expect(consoleSpy).toHaveBeenCalled();
});
it('should skip plugin without afterReport', () => {
const manager = new PluginManager(mockClient);
const plugin: LightPlugin = {
name: 'no-after',
version: '1.0.0',
setup: vi.fn(),
};
manager.add(plugin);
const event = { type: 'error', level: 'error', timestamp: Date.now() } as SentryEvent;
expect(() => manager.applyAfterReport(event)).not.toThrow();
});
});
describe('destroy', () => {
it('should call destroy on all plugins', () => {
const manager = new PluginManager(mockClient);
const destroy1 = vi.fn();
const destroy2 = vi.fn();
const plugin1: LightPlugin = {
name: 'destroy-1',
version: '1.0.0',
setup: vi.fn(),
destroy: destroy1,
};
const plugin2: LightPlugin = {
name: 'destroy-2',
version: '1.0.0',
setup: vi.fn(),
destroy: destroy2,
};
manager.add(plugin1);
manager.add(plugin2);
manager.destroy();
expect(destroy1).toHaveBeenCalled();
expect(destroy2).toHaveBeenCalled();
});
it('should clear plugins after destroy', () => {
const manager = new PluginManager(mockClient);
const plugin: LightPlugin = {
name: 'clear-me',
version: '1.0.0',
setup: vi.fn(),
};
manager.add(plugin);
expect(manager.has('clear-me')).toBe(true);
manager.destroy();
expect(manager.has('clear-me')).toBe(false);
});
it('should continue even if destroy throws', () => {
const manager = new PluginManager(mockClient);
const plugin1: LightPlugin = {
name: 'destroy-error',
version: '1.0.0',
setup: vi.fn(),
destroy: () => {
throw new Error('Destroy error');
},
};
const plugin2: LightPlugin = {
name: 'destroy-ok',
version: '1.0.0',
setup: vi.fn(),
destroy: vi.fn(),
};
manager.add(plugin1);
manager.add(plugin2);
manager.destroy();
expect(consoleSpy).toHaveBeenCalled();
expect((plugin2 as any).destroy).toHaveBeenCalled();
});
it('should not throw if plugin has no destroy method', () => {
const manager = new PluginManager(mockClient);
const plugin: LightPlugin = {
name: 'no-destroy',
version: '1.0.0',
setup: vi.fn(),
};
manager.add(plugin);
expect(() => manager.destroy()).not.toThrow();
});
});
});

View File

@ -0,0 +1,272 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import SamplingPlugin from '../src/plugins/SamplingPlugin';
import type { LightClient, LightConfig, SentryEvent, ErrorEvent } from '../src/types';
describe('SamplingPlugin', () => {
function createMockClient(configOverrides: Partial<LightConfig> = {}): LightClient {
const config: LightConfig = {
dsn: 'https://abc@sentry.io/123',
sampleRate: 1,
sampling: {
rates: {
error: 1,
performance: 1,
network: 1,
behavior: 1,
},
},
...configOverrides,
} as LightConfig;
return {
config,
dsn: { protocol: 'https', publicKey: 'abc', host: 'sentry.io', projectId: '123' },
on: vi.fn(),
off: vi.fn(),
emit: vi.fn(),
captureException: vi.fn(),
captureMessage: vi.fn(),
captureEvent: vi.fn(),
setUser: vi.fn(),
setAnonymousId: vi.fn(),
setTag: vi.fn(),
setTags: vi.fn(),
setExtra: vi.fn(),
addBreadcrumb: vi.fn(),
startTransaction: vi.fn(),
getCurrentTransaction: vi.fn(),
getCurrentSpan: vi.fn(),
setSpan: vi.fn(),
startSpan: vi.fn(),
flush: vi.fn(),
disable: vi.fn(),
enable: vi.fn(),
use: vi.fn(),
init: vi.fn(),
destroy: vi.fn(),
} as unknown as LightClient;
}
function createErrorEvent(): ErrorEvent {
return {
type: 'error',
level: 'error',
message: 'Test error',
timestamp: Date.now(),
};
}
function createEvent(type: string): SentryEvent {
return {
type: type as any,
level: 'info',
timestamp: Date.now(),
} as SentryEvent;
}
describe('constructor', () => {
it('should create plugin with correct name and version', () => {
const plugin = new SamplingPlugin();
expect(plugin.name).toBe('sampling');
expect(plugin.version).toBe('1.0.0');
});
});
describe('setup', () => {
it('should call setup with client', () => {
const plugin = new SamplingPlugin();
const client = createMockClient();
expect(() => plugin.setup(client)).not.toThrow();
});
it('should load default rates when no sampling config', () => {
const plugin = new SamplingPlugin();
const client = createMockClient({ sampling: undefined });
plugin.setup(client);
const event = createErrorEvent();
const result = plugin.beforeReport(event);
expect(result).not.toBeNull();
});
it('should load custom rates from config', () => {
const plugin = new SamplingPlugin();
const client = createMockClient({
sampleRate: 1,
sampling: {
rates: {
error: 0.5,
performance: 0.1,
},
},
});
plugin.setup(client);
const randomSpy = vi.spyOn(Math, 'random').mockReturnValue(0.1);
expect(plugin.beforeReport(createEvent('error'))).toBeTruthy();
randomSpy.mockRestore();
});
});
describe('beforeReport', () => {
it('should return event when sampling rate is 1', () => {
const plugin = new SamplingPlugin();
const client = createMockClient({ sampleRate: 1 });
plugin.setup(client);
const event = createErrorEvent();
const result = plugin.beforeReport(event);
expect(result).toBe(event);
});
it('should return null when global sampleRate is 0', () => {
const plugin = new SamplingPlugin();
const client = createMockClient({ sampleRate: 0 });
plugin.setup(client);
const event = createErrorEvent();
const result = plugin.beforeReport(event);
expect(result).toBeNull();
});
it('should return null when global sampleRate is negative', () => {
const plugin = new SamplingPlugin();
const client = createMockClient({ sampleRate: -1 });
plugin.setup(client);
const event = createErrorEvent();
const result = plugin.beforeReport(event);
expect(result).toBeNull();
});
it('should return event when type rate is 1', () => {
const plugin = new SamplingPlugin();
const client = createMockClient({
sampleRate: 1,
sampling: { rates: { error: 1 } },
});
plugin.setup(client);
const event = createErrorEvent();
const result = plugin.beforeReport(event);
expect(result).toBe(event);
});
it('should return null when type rate is 0', () => {
const plugin = new SamplingPlugin();
const client = createMockClient({
sampleRate: 1,
sampling: { rates: { error: 0 } },
});
plugin.setup(client);
const event = createErrorEvent();
const result = plugin.beforeReport(event);
expect(result).toBeNull();
});
it('should always sample count events (rate = 1)', () => {
const plugin = new SamplingPlugin();
const client = createMockClient({
sampleRate: 0,
sampling: { rates: { error: 0 } },
});
plugin.setup(client);
const countEvent = {
type: 'count' as const,
level: 'info' as const,
timestamp: Date.now(),
fingerprint: 'test',
count: 1,
ts_start: Date.now(),
ts_end: Date.now(),
};
const result = plugin.beforeReport(countEvent);
expect(result).toBeNull();
});
it('should handle unknown event types (default rate = 1)', () => {
const plugin = new SamplingPlugin();
const client = createMockClient({ sampleRate: 1 });
plugin.setup(client);
const unknownEvent = createEvent('unknown_type');
const result = plugin.beforeReport(unknownEvent);
expect(result).not.toBeNull();
});
it('should use combined rate when globalRate < 1', () => {
const plugin = new SamplingPlugin();
const client = createMockClient({
sampleRate: 0.5,
sampling: { rates: { error: 0.5 } },
});
plugin.setup(client);
const randomSpy = vi.spyOn(Math, 'random').mockReturnValue(0.9);
const event = createErrorEvent();
const result = plugin.beforeReport(event);
expect(result).toBeNull();
randomSpy.mockRestore();
});
it('should pass when random is below combined rate', () => {
const plugin = new SamplingPlugin();
const client = createMockClient({
sampleRate: 0.5,
sampling: { rates: { error: 0.5 } },
});
plugin.setup(client);
const randomSpy = vi.spyOn(Math, 'random').mockReturnValue(0.1);
const event = createErrorEvent();
const result = plugin.beforeReport(event);
expect(result).toBe(event);
randomSpy.mockRestore();
});
it('should pass when type rate is 1 and globalRate is 1', () => {
const plugin = new SamplingPlugin();
const client = createMockClient({ sampleRate: 1 });
plugin.setup(client);
const randomSpy = vi.spyOn(Math, 'random').mockReturnValue(0.99);
const event = createErrorEvent();
const result = plugin.beforeReport(event);
expect(result).toBe(event);
randomSpy.mockRestore();
});
});
describe('destroy', () => {
it('should not throw when destroy is called', () => {
const plugin = new SamplingPlugin();
expect(() => plugin.destroy()).not.toThrow();
});
});
describe('config loading', () => {
it('should handle missing sampling config', () => {
const plugin = new SamplingPlugin();
const client = createMockClient({ sampling: undefined });
plugin.setup(client);
const event = createErrorEvent();
const result = plugin.beforeReport(event);
expect(result).not.toBeNull();
});
it('should handle sampling config without rates', () => {
const plugin = new SamplingPlugin();
const client = createMockClient({ sampling: {} });
plugin.setup(client);
const event = createErrorEvent();
const result = plugin.beforeReport(event);
expect(result).not.toBeNull();
});
it('should merge custom rates with defaults', () => {
const plugin = new SamplingPlugin();
const client = createMockClient({
sampling: {
rates: {
error: 0.5,
},
},
});
plugin.setup(client);
const randomSpy = vi.spyOn(Math, 'random').mockReturnValue(0.9);
expect(plugin.beforeReport(createEvent('error'))).toBeNull();
expect(plugin.beforeReport(createEvent('performance'))).not.toBeNull();
randomSpy.mockRestore();
});
});
});

View File

@ -0,0 +1,350 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { TracingManager } from '../src/core/TracingManager';
import type { LightConfig, Transaction, Span } from '../src/types';
describe('TracingManager', () => {
let tracingManager: TracingManager;
let mockClient: ReturnType<typeof createMockClient>;
beforeEach(() => {
mockClient = createMockClient();
const config: LightConfig = {
dsn: 'https://abc@log.example.com/1001',
tracesSampleRate: 1,
};
tracingManager = new TracingManager(config, mockClient, 'abc');
});
afterEach(() => {
tracingManager.destroy();
vi.clearAllMocks();
});
describe('startTransaction', () => {
it('should create transaction with correct properties', () => {
const transaction = tracingManager.startTransaction({
name: 'test-transaction',
op: 'custom',
});
expect(transaction).toBeDefined();
expect(transaction?.name).toBe('test-transaction');
expect(transaction?.op).toBe('custom');
expect(transaction?.traceId).toBeDefined();
expect(transaction?.spanId).toBeDefined();
expect(transaction?.sampled).toBe(true); // tracesSampleRate = 1
});
it('should return transaction even when SDK would be disabled (TracingManager does not check enabled)', () => {
// Note: Client.startTransaction checks enabled, but TracingManager does not
const disabledConfig: LightConfig = {
dsn: 'https://abc@log.example.com/1001',
enabled: false,
tracesSampleRate: 1,
};
const disabledManager = new TracingManager(disabledConfig, mockClient, 'abc');
const transaction = disabledManager.startTransaction({
name: 'test-transaction',
});
// TracingManager doesn't know about enabled flag, it still creates transaction
expect(transaction).toBeDefined();
disabledManager.destroy();
});
it('should sample transaction based on tracesSampleRate', () => {
const sampledConfig: LightConfig = {
dsn: 'https://abc@log.example.com/1001',
tracesSampleRate: 0, // No sampling
};
const sampledManager = new TracingManager(sampledConfig, mockClient, 'abc');
const transaction = sampledManager.startTransaction({
name: 'test-transaction',
});
expect(transaction?.sampled).toBe(false);
sampledManager.destroy();
});
it('should use tracesSampler for dynamic sampling', () => {
const customSamplerConfig: LightConfig = {
dsn: 'https://abc@log.example.com/1001',
tracesSampler: () => false,
};
const customSamplerManager = new TracingManager(customSamplerConfig, mockClient, 'abc');
const transaction = customSamplerManager.startTransaction({
name: 'test-transaction',
});
expect(transaction?.sampled).toBe(false);
customSamplerManager.destroy();
});
it('should set current transaction after start', () => {
const transaction = tracingManager.startTransaction({
name: 'test-transaction',
});
expect(tracingManager.getCurrentTransaction()).toBe(transaction);
});
it('should use tracesSampler for dynamic sampling with parentSampled', () => {
// With tracesSampler, parentSampled is passed to the sampler
const customSamplerConfig: LightConfig = {
dsn: 'https://abc@log.example.com/1001',
tracesSampler: (ctx) => {
// If parent was sampled=false, child should also be not sampled
if (ctx.parentSampled === false) {
return false;
}
return true;
},
};
const parentSamplerManager = new TracingManager(customSamplerConfig, mockClient, 'abc');
const parentTransaction = parentSamplerManager.startTransaction({
name: 'parent',
sampled: false,
});
const childTransaction = parentSamplerManager.startTransaction({
name: 'child',
sampled: undefined,
});
// Child should inherit parent's sampled=false via tracesSampler
expect(childTransaction?.sampled).toBe(false);
parentSamplerManager.destroy();
});
});
describe('getCurrentTransaction', () => {
it('should return null when no transaction exists', () => {
expect(tracingManager.getCurrentTransaction()).toBeNull();
});
it('should return current transaction', () => {
const transaction = tracingManager.startTransaction({
name: 'test-transaction',
});
expect(tracingManager.getCurrentTransaction()).toBe(transaction);
});
it('should return current transaction after finish (via TracingManager.onTransactionFinished)', () => {
// Note: Transaction.finish() calls client.tracingManager.onTransactionFinished
// if client is properly set up. With mock client, this doesn't happen automatically.
const transaction = tracingManager.startTransaction({
name: 'test-transaction',
});
expect(tracingManager.getCurrentTransaction()).toBe(transaction);
// Manually call onTransactionFinished to simulate proper cleanup
tracingManager.onTransactionFinished(transaction!);
expect(tracingManager.getCurrentTransaction()).toBeNull();
});
});
describe('getCurrentSpan', () => {
it('should return transaction as span when no child spans', () => {
const transaction = tracingManager.startTransaction({
name: 'test-transaction',
});
const currentSpan = tracingManager.getCurrentSpan();
expect(currentSpan?.spanId).toBe(transaction?.spanId);
});
it('should return most recent child span', () => {
const transaction = tracingManager.startTransaction({
name: 'test-transaction',
});
const childSpan = tracingManager.startSpan({
op: 'child.span',
description: 'child operation',
});
const currentSpan = tracingManager.getCurrentSpan();
expect(currentSpan?.spanId).toBe(childSpan?.spanId);
});
});
describe('startSpan', () => {
it('should create child span under current transaction', () => {
tracingManager.startTransaction({
name: 'test-transaction',
});
const childSpan = tracingManager.startSpan({
op: 'http.client',
description: 'GET /api/users',
});
expect(childSpan).toBeDefined();
expect(childSpan?.op).toBe('http.client');
expect(childSpan?.parentSpanId).toBeDefined();
});
it('should return null when no active span and no traceId', () => {
const noSpanManager = new TracingManager(
{ dsn: 'https://abc@log.example.com/1001' },
mockClient,
'abc'
);
const span = noSpanManager.startSpan({
op: 'test.span',
});
expect(span).toBeNull();
noSpanManager.destroy();
});
it('should create span with traceId when no parent span exists', () => {
const noSpanManager = new TracingManager(
{ dsn: 'https://abc@log.example.com/1001' },
mockClient,
'abc'
);
const span = noSpanManager.startSpan({
op: 'test.span',
traceId: 'abcd1234abcd1234abcd1234abcd1234',
});
expect(span).toBeDefined();
expect(span?.traceId).toBe('abcd1234abcd1234abcd1234abcd1234');
noSpanManager.destroy();
});
it('should not add span to unsampled transaction', () => {
const unsampledManager = new TracingManager(
{ dsn: 'https://abc@log.example.com/1001', tracesSampleRate: 0 },
mockClient,
'abc'
);
const transaction = unsampledManager.startTransaction({ name: 'unsampled' });
const span = unsampledManager.startSpan({ op: 'child' });
expect(span).toBeDefined();
// Span should not be in transaction.spans for unsampled transaction
expect(transaction?.spans.length).toBe(0);
unsampledManager.destroy();
});
});
describe('setCurrentSpan', () => {
it('should set current span', () => {
tracingManager.startTransaction({ name: 'test' });
const customSpan = { spanId: 'custom-span' } as Span;
tracingManager.setCurrentSpan(customSpan);
expect(tracingManager.getCurrentSpan()?.spanId).toBe('custom-span');
});
it('should pop span from stack when set to null', () => {
tracingManager.startTransaction({ name: 'test' });
const childSpan = tracingManager.startSpan({ op: 'child' });
expect(tracingManager.getCurrentSpan()?.spanId).toBe(childSpan?.spanId);
tracingManager.setCurrentSpan(null);
// Should return to transaction
expect(tracingManager.getCurrentSpan()?.spanId).toBeDefined();
});
});
describe('onSpanFinished', () => {
it('should remove finished span from stack', () => {
tracingManager.startTransaction({ name: 'test' });
const childSpan = tracingManager.startSpan({ op: 'child' });
expect(tracingManager.getCurrentSpan()?.spanId).toBe(childSpan?.spanId);
tracingManager.onSpanFinished(childSpan!);
// Should return to transaction after child is removed
expect(tracingManager.getCurrentSpan()?.spanId).toBeDefined();
});
});
describe('onTransactionFinished', () => {
it('should clear current transaction', () => {
const transaction = tracingManager.startTransaction({ name: 'test' });
expect(tracingManager.getCurrentTransaction()).toBe(transaction);
tracingManager.onTransactionFinished(transaction!);
expect(tracingManager.getCurrentTransaction()).toBeNull();
});
});
describe('updateConfig', () => {
it('should update baggage when user changes', () => {
const transaction = tracingManager.startTransaction({ name: 'test' });
expect(transaction).toBeDefined();
tracingManager.updateConfig({
dsn: 'https://abc@log.example.com/1001',
user: { id: 'user123', segment: 'premium' },
});
// Transaction baggage should have user info
// This is an internal detail, just verify no error
expect(transaction).toBeDefined();
});
});
describe('destroy', () => {
it('should finish current transaction on destroy', () => {
const transaction = tracingManager.startTransaction({ name: 'test' });
tracingManager.destroy();
// Transaction should be finished (sampled=false since no client capture)
expect(tracingManager.getCurrentTransaction()).toBeNull();
});
it('should clear span stack on destroy', () => {
tracingManager.startTransaction({ name: 'test' });
tracingManager.startSpan({ op: 'child' });
tracingManager.destroy();
expect(tracingManager.getCurrentSpan()).toBeNull();
});
});
});
function createMockClient() {
return {
config: {},
dsn: { protocol: 'https', publicKey: 'abc', host: 'log.example.com', projectId: '1001' },
captureException: vi.fn(),
captureMessage: vi.fn(),
captureEvent: vi.fn(),
setUser: vi.fn(),
setAnonymousId: vi.fn(),
setTag: vi.fn(),
setTags: vi.fn(),
setExtra: vi.fn(),
addBreadcrumb: vi.fn(),
startTransaction: vi.fn(),
getCurrentTransaction: vi.fn(),
getCurrentSpan: vi.fn(),
setSpan: vi.fn(),
startSpan: vi.fn(),
flush: vi.fn(),
disable: vi.fn(),
enable: vi.fn(),
use: vi.fn(),
init: vi.fn(),
destroy: vi.fn(),
on: vi.fn(),
off: vi.fn(),
emit: vi.fn(),
} as any;
}

250
tests/baggage.test.ts Normal file
View File

@ -0,0 +1,250 @@
import { describe, it, expect } from 'vitest';
import {
serializeBaggage,
parseBaggage,
createBaggageFromTransaction,
} from '../src/utils/baggage';
import type { BaggageFields } from '../src/types';
describe('baggage utils', () => {
describe('serializeBaggage', () => {
it('should serialize empty baggage', () => {
const result = serializeBaggage({});
expect(result).toBe('');
});
it('should serialize baggage with all fields', () => {
const baggage: BaggageFields = {
trace_id: 'abc123',
public_key: 'key123',
sample_rate: '0.5',
sampled: 'true',
user_id: 'user1',
user_segment: 'premium',
release: '1.0.0',
environment: 'production',
};
const result = serializeBaggage(baggage);
expect(result).toContain('sentry-trace_id=abc123');
expect(result).toContain('sentry-public_key=key123');
expect(result).toContain('sentry-sample_rate=0.5');
expect(result).toContain('sentry-sampled=true');
expect(result).toContain('sentry-user_id=user1');
expect(result).toContain('sentry-user_segment=premium');
expect(result).toContain('sentry-release=1.0.0');
expect(result).toContain('sentry-environment=production');
});
it('should skip null, undefined, and empty string values', () => {
const baggage: BaggageFields = {
trace_id: 'abc123',
public_key: undefined as any,
sampled: '',
};
const result = serializeBaggage(baggage);
expect(result).toContain('sentry-trace_id=abc123');
expect(result).not.toContain('public_key');
expect(result).not.toContain('sampled');
});
it('should add sentry- prefix to non-sentry keys', () => {
const baggage: BaggageFields = {
trace_id: 'abc123',
custom_field: 'value',
};
const result = serializeBaggage(baggage);
expect(result).toContain('sentry-trace_id=abc123');
expect(result).toContain('sentry-custom_field=value');
});
it('should preserve sentry- prefix on sentry keys', () => {
const baggage: BaggageFields = {
'sentry-trace_id': 'abc123',
};
const result = serializeBaggage(baggage);
expect(result).toContain('sentry-trace_id=abc123');
// Should not double-prefix
expect(result).not.toContain('sentry-sentry-');
});
it('should truncate long baggage to 8192 characters', () => {
const longBaggage: BaggageFields = {};
// Create a very long value
const longValue = 'a'.repeat(10000);
longBaggage.trace_id = longValue;
const result = serializeBaggage(longBaggage);
expect(result.length).toBeLessThanOrEqual(8192);
});
it('should URL encode values', () => {
const baggage: BaggageFields = {
user_id: 'user with spaces',
custom_field: 'value=special&chars',
};
const result = serializeBaggage(baggage);
expect(result).toContain(encodeURIComponent('user with spaces'));
expect(result).toContain(encodeURIComponent('value=special&chars'));
});
});
describe('parseBaggage', () => {
it('should parse empty baggage', () => {
const result = parseBaggage('');
expect(result).toEqual({});
});
it('should parse valid baggage header', () => {
const header = 'sentry-trace_id=abc123,sentry-sampled=true,sentry-user_id=user1';
const result = parseBaggage(header);
expect(result.trace_id).toBe('abc123');
expect(result.sampled).toBe('true');
expect(result.user_id).toBe('user1');
});
it('should remove sentry- prefix from keys', () => {
const header = 'sentry-public_key=key123,sentry-release=1.0.0';
const result = parseBaggage(header);
expect(result.public_key).toBe('key123');
expect(result.release).toBe('1.0.0');
expect(result['sentry-public_key']).toBeUndefined();
});
it('should handle custom fields without prefix', () => {
const header = 'custom_field=value,another=data';
const result = parseBaggage(header);
expect(result.custom_field).toBe('value');
expect(result.another).toBe('data');
});
it('should URL decode values', () => {
const header = `sentry-user_id=${encodeURIComponent('user with spaces')}`;
const result = parseBaggage(header);
expect(result.user_id).toBe('user with spaces');
});
it('should skip malformed entries', () => {
const header = 'valid=key,invalid-entry-no-equals,sentry-trace_id=abc123';
const result = parseBaggage(header);
expect(result.valid).toBe('key');
expect(result['sentry-trace_id']).toBeUndefined();
expect(result.trace_id).toBe('abc123');
});
it('should handle whitespace around entries', () => {
const header = ' sentry-trace_id=abc123 , sentry-sampled=true ';
const result = parseBaggage(header);
expect(result.trace_id).toBe('abc123');
expect(result.sampled).toBe('true');
});
});
describe('createBaggageFromTransaction', () => {
it('should create baggage with only trace_id', () => {
const result = createBaggageFromTransaction('abc123');
expect(result.trace_id).toBe('abc123');
expect(Object.keys(result)).toHaveLength(1);
});
it('should create baggage with all options', () => {
const result = createBaggageFromTransaction('abc123', {
dsnPublicKey: 'key123',
sampled: true,
sampleRate: 0.5,
user: { id: 'user1', segment: 'premium' },
release: '1.0.0',
environment: 'production',
});
expect(result.trace_id).toBe('abc123');
expect(result.public_key).toBe('key123');
expect(result.sampled).toBe('true');
expect(result.sample_rate).toBe('0.5');
expect(result.user_id).toBe('user1');
expect(result.user_segment).toBe('premium');
expect(result.release).toBe('1.0.0');
expect(result.environment).toBe('production');
});
it('should convert sampled boolean to string', () => {
expect(createBaggageFromTransaction('abc', { sampled: true }).sampled).toBe('true');
expect(createBaggageFromTransaction('abc', { sampled: false }).sampled).toBe('false');
});
it('should convert sampleRate number to string', () => {
const result = createBaggageFromTransaction('abc', { sampleRate: 0.1 });
expect(result.sample_rate).toBe('0.1');
});
it('should handle optional user object', () => {
const result1 = createBaggageFromTransaction('abc', { user: { id: 'user1' } });
expect(result1.user_id).toBe('user1');
expect(result1.user_segment).toBeUndefined();
const result2 = createBaggageFromTransaction('abc', { user: { id: 'user2', segment: 'vip' } });
expect(result2.user_id).toBe('user2');
expect(result2.user_segment).toBe('vip');
});
it('should not include undefined options', () => {
const result = createBaggageFromTransaction('abc', {
dsnPublicKey: 'key',
sampled: undefined,
sampleRate: undefined,
});
expect(result.trace_id).toBe('abc');
expect(result.public_key).toBe('key');
expect(result.sampled).toBeUndefined();
expect(result.sample_rate).toBeUndefined();
});
it('should work without options', () => {
const result = createBaggageFromTransaction('abc');
expect(result.trace_id).toBe('abc');
});
});
describe('serialize/parse roundtrip', () => {
it('should preserve baggage through serialize/parse cycle', () => {
const original: BaggageFields = {
trace_id: 'abc123',
public_key: 'key123',
sampled: 'true',
user_id: 'user1',
release: '1.0.0',
environment: 'production',
custom: 'value',
};
const serialized = serializeBaggage(original);
const parsed = parseBaggage(serialized);
expect(parsed.trace_id).toBe(original.trace_id);
expect(parsed.public_key).toBe(original.public_key);
expect(parsed.sampled).toBe(original.sampled);
expect(parsed.user_id).toBe(original.user_id);
expect(parsed.release).toBe(original.release);
expect(parsed.environment).toBe(original.environment);
expect(parsed.custom).toBe(original.custom);
});
});
});

View File

@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { import {
getEnvInfo, getEnvInfo,
getContexts, getContexts,
@ -10,6 +10,17 @@ import {
} from '../src/utils/env'; } from '../src/utils/env';
describe('env utils', () => { describe('env utils', () => {
const originalNavigator = global.navigator;
const originalUserAgent = navigator.userAgent;
function setUserAgent(ua: string) {
Object.defineProperty(global.navigator, 'userAgent', {
value: ua,
configurable: true,
writable: true,
});
}
describe('getEnvInfo', () => { describe('getEnvInfo', () => {
it('should return env info object', () => { it('should return env info object', () => {
const env = getEnvInfo(); const env = getEnvInfo();
@ -38,6 +49,158 @@ describe('env utils', () => {
}); });
}); });
describe('browser detection', () => {
const testCases = [
{
name: 'Chrome on Windows',
ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
browser: 'Chrome',
os: 'Windows 11',
device: 'desktop',
},
{
name: 'Firefox on macOS',
ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0',
browser: 'Firefox',
os: 'macOS',
device: 'desktop',
},
{
name: 'Safari on macOS',
ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15',
browser: 'Safari',
os: 'macOS',
device: 'desktop',
},
{
name: 'Edge on Windows',
ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0',
browser: 'Edge',
os: 'Windows 11',
device: 'desktop',
},
{
name: 'WeChat',
ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.33(0x1800212d)',
browser: 'WeChat',
os: 'iOS',
device: 'mobile',
},
{
name: 'iPhone Safari',
ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
browser: 'Safari',
os: 'iOS',
device: 'mobile',
},
{
name: 'iPad Safari',
ua: 'Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
browser: 'Safari',
os: 'iOS',
device: 'tablet',
},
{
name: 'Android Chrome',
ua: 'Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
browser: 'Chrome',
os: 'Android',
device: 'mobile',
},
{
name: 'Samsung device',
ua: 'Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
browser: 'Chrome',
os: 'Android',
device: 'mobile',
vendor: 'Samsung',
},
{
name: 'Opera',
ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 OPR/106.0.0.0',
browser: 'Opera',
os: 'Windows 11',
device: 'desktop',
},
];
testCases.forEach(tc => {
it(`should detect ${tc.name}`, () => {
setUserAgent(tc.ua);
const env = getEnvInfo();
expect(env.browser.name).toBe(tc.browser);
expect(env.os.name).toBe(tc.os);
expect(env.device.type).toBe(tc.device);
});
});
it('should detect Unknown browser for unrecognized UA', () => {
setUserAgent('SomeCustomBot/1.0');
const env = getEnvInfo();
expect(env.browser.name).toBe('Unknown');
});
it('should detect Windows 11', () => {
setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
// The current code doesn't distinguish Win10 vs Win11 based on Win64 x64 alone
const env = getEnvInfo();
expect(env.os.name).toMatch(/Windows/);
});
it('should parse browser version', () => {
setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.5481.100 Safari/537.36');
const env = getEnvInfo();
expect(env.browser.version).toMatch(/^120/);
expect(env.browser.major).toBe('120');
});
it('should parse macOS version with underscores', () => {
setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15');
const env = getEnvInfo();
expect(env.os.name).toBe('macOS');
expect(env.os.version).toContain('14.2.1');
});
});
describe('device detection', () => {
it('should detect iPhone as mobile Apple device', () => {
setUserAgent('Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1');
const env = getEnvInfo();
expect(env.device.type).toBe('mobile');
expect(env.device.vendor).toBe('Apple');
expect(env.device.model).toBe('iPhone');
});
it('should detect iPad as tablet Apple device', () => {
setUserAgent('Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1');
const env = getEnvInfo();
expect(env.device.type).toBe('tablet');
expect(env.device.vendor).toBe('Apple');
expect(env.device.model).toBe('iPad');
});
it('should detect Samsung device', () => {
setUserAgent('Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36');
const env = getEnvInfo();
expect(env.device.vendor).toBe('Samsung');
expect(env.device.model).toBe('G991B');
});
it('should detect Pixel device', () => {
setUserAgent('Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36');
const env = getEnvInfo();
expect(env.device.vendor).toBe('Google');
expect(env.device.model).toBe('Pixel 8');
});
it('should detect Huawei device', () => {
setUserAgent('Mozilla/5.0 (Linux; Android 12; HUAWEI Mate40 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36');
const env = getEnvInfo();
expect(env.device.vendor).toBe('Huawei');
expect(env.device.model).toBe('Mate40');
});
});
describe('getContexts', () => { describe('getContexts', () => {
it('should return contexts with encoded values', () => { it('should return contexts with encoded values', () => {
const contexts = getContexts(); const contexts = getContexts();
@ -56,13 +219,26 @@ describe('env utils', () => {
const contexts = getContexts(); const contexts = getContexts();
expect(contexts.os.name.length).toBeLessThanOrEqual(10); expect(contexts.os.name.length).toBeLessThanOrEqual(10);
}); });
it('should map Chrome to c', () => {
setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
const contexts = getContexts();
expect(contexts.browser.name).toBe('c');
});
it('should map Windows 11 to w11', () => {
setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
const contexts = getContexts();
expect(contexts.os.name).toBe('w11');
});
}); });
describe('getContextId', () => { describe('getContextId', () => {
it('should return a 16 character string', () => { it('should return a hex string of up to 8 characters (32-bit hash)', () => {
const contextId = getContextId(); const contextId = getContextId();
expect(contextId).toBeDefined(); expect(contextId).toBeDefined();
expect(contextId.length).toBe(16); expect(contextId.length).toBeLessThanOrEqual(16);
expect(contextId.length).toBeGreaterThan(0);
expect(contextId).toMatch(/^[a-f0-9]+$/i); expect(contextId).toMatch(/^[a-f0-9]+$/i);
}); });
@ -121,6 +297,49 @@ describe('env utils', () => {
expect(sanitized).toContain('code'); expect(sanitized).toContain('code');
expect(sanitized).toContain('%5BFiltered%5D'); expect(sanitized).toContain('%5BFiltered%5D');
}); });
it('should filter sensitive params in hash', () => {
const url = 'https://example.com/page#access_token=secret123&other=value';
const sanitized = sanitizeUrl(url);
expect(sanitized).toContain('access_token');
expect(sanitized).toContain('%5BFiltered%5D');
});
it('should filter javascript: protocol', () => {
const url = 'javascript:alert(1)';
const sanitized = sanitizeUrl(url);
expect(sanitized).toBe('javascript:[filtered]');
});
it('should filter javascript: with whitespace', () => {
const url = ' javascript:alert(document.cookie)';
const sanitized = sanitizeUrl(url);
expect(sanitized).toBe('javascript:[filtered]');
});
it('should handle empty string', () => {
const sanitized = sanitizeUrl('');
expect(sanitized).toBe('');
});
it('should handle null/undefined', () => {
expect(sanitizeUrl(null as any)).toBeNull();
expect(sanitizeUrl(undefined as any)).toBeUndefined();
});
it('should filter multiple sensitive params', () => {
const url = 'https://example.com/api?password=pwd&token=tkn&api_key=key&secret=sec&session=sess';
const sanitized = sanitizeUrl(url);
expect(sanitized).toContain('%5BFiltered%5D');
const filteredCount = (sanitized.match(/%5BFiltered%5D/g) || []).length;
expect(filteredCount).toBe(5);
});
it('should keep non-sensitive params unchanged', () => {
const url = 'https://example.com/api?page=1&limit=10&sort=desc';
const sanitized = sanitizeUrl(url);
expect(sanitized).toBe(url);
});
}); });
describe('shouldIgnoreUrl', () => { describe('shouldIgnoreUrl', () => {
@ -153,5 +372,18 @@ describe('env utils', () => {
const result = shouldIgnoreUrl('https://example.com/api/users/123', [/api\/users\/\d+/]); const result = shouldIgnoreUrl('https://example.com/api/users/123', [/api\/users\/\d+/]);
expect(result).toBe(true); expect(result).toBe(true);
}); });
it('should match multiple patterns (OR logic)', () => {
const patterns = ['/health', '/metrics', /\/debug/];
expect(shouldIgnoreUrl('https://example.com/health', patterns)).toBe(true);
expect(shouldIgnoreUrl('https://example.com/api/metrics', patterns)).toBe(true);
expect(shouldIgnoreUrl('https://example.com/debug/vars', patterns)).toBe(true);
expect(shouldIgnoreUrl('https://example.com/api/users', patterns)).toBe(false);
});
it('should match wildcard with multiple *', () => {
const result = shouldIgnoreUrl('https://api.example.com/v1/users/123/info', ['*.example.com/*/users/*']);
expect(result).toBe(true);
});
}); });
}); });

239
tests/field-encoder.test.ts Normal file
View File

@ -0,0 +1,239 @@
import { describe, it, expect } from 'vitest';
import {
FIELD_ENCODE_MAP,
FIELD_DECODE_MAP,
encodeFields,
decodeFields,
} from '../src/utils/field-encoder';
describe('field-encoder', () => {
describe('FIELD_ENCODE_MAP', () => {
it('should contain mappings for all basic fields', () => {
expect(FIELD_ENCODE_MAP.event_id).toBe('eid');
expect(FIELD_ENCODE_MAP.timestamp).toBe('ts');
expect(FIELD_ENCODE_MAP.start_timestamp).toBe('sts');
expect(FIELD_ENCODE_MAP.type).toBe('t');
expect(FIELD_ENCODE_MAP.level).toBe('l');
expect(FIELD_ENCODE_MAP.message).toBe('m');
expect(FIELD_ENCODE_MAP.platform).toBe('p');
expect(FIELD_ENCODE_MAP.release).toBe('r');
expect(FIELD_ENCODE_MAP.environment).toBe('e');
expect(FIELD_ENCODE_MAP.fingerprint).toBe('fp');
expect(FIELD_ENCODE_MAP.context_id).toBe('cid');
});
it('should contain mappings for user fields', () => {
expect(FIELD_ENCODE_MAP.user).toBe('u');
});
it('should contain mappings for tags', () => {
expect(FIELD_ENCODE_MAP.tags).toBe('tg');
});
it('should contain mappings for exception', () => {
expect(FIELD_ENCODE_MAP.exception).toBe('ex');
});
it('should contain mappings for request', () => {
expect(FIELD_ENCODE_MAP.request).toBe('req');
});
it('should contain mappings for contexts', () => {
expect(FIELD_ENCODE_MAP.contexts).toBe('ctx');
});
it('should contain mappings for breadcrumbs', () => {
expect(FIELD_ENCODE_MAP.breadcrumbs).toBe('bc');
});
it('should contain mappings for transaction', () => {
expect(FIELD_ENCODE_MAP.transaction).toBe('tx');
expect(FIELD_ENCODE_MAP.duration).toBe('d');
expect(FIELD_ENCODE_MAP.spans).toBe('sp');
});
it('should contain mappings for extra', () => {
expect(FIELD_ENCODE_MAP.extra).toBe('xt');
});
});
describe('FIELD_DECODE_MAP', () => {
it('should be reverse of FIELD_ENCODE_MAP', () => {
for (const [key, value] of Object.entries(FIELD_ENCODE_MAP)) {
expect(FIELD_DECODE_MAP[value]).toBe(key);
}
});
it('should decode short codes back to original field names', () => {
expect(FIELD_DECODE_MAP['eid']).toBe('event_id');
expect(FIELD_DECODE_MAP['ts']).toBe('timestamp');
expect(FIELD_DECODE_MAP['t']).toBe('type');
expect(FIELD_DECODE_MAP['l']).toBe('level');
expect(FIELD_DECODE_MAP['m']).toBe('message');
});
});
describe('encodeFields', () => {
it('should encode basic fields to short codes', () => {
const event = {
event_id: 'abc123',
timestamp: '2024-01-01T00:00:00.000Z',
type: 'error',
level: 'error',
message: 'Test error',
};
const encoded = encodeFields(event);
expect(encoded.eid).toBe('abc123');
expect(encoded.ts).toBe('2024-01-01T00:00:00.000Z');
expect(encoded.t).toBe('error');
expect(encoded.l).toBe('error');
expect(encoded.m).toBe('Test error');
});
it('should keep unknown fields unchanged', () => {
const event = {
event_id: 'abc123',
custom_field: 'custom_value',
another_field: 123,
};
const encoded = encodeFields(event);
expect(encoded.eid).toBe('abc123');
expect(encoded.custom_field).toBe('custom_value');
expect(encoded.another_field).toBe(123);
});
it('should return non-object input unchanged', () => {
expect(encodeFields(null as any)).toBe(null);
expect(encodeFields(undefined as any)).toBe(undefined);
expect(encodeFields(123 as any)).toBe(123);
expect(encodeFields('string' as any)).toBe('string');
});
it('should handle empty object', () => {
const encoded = encodeFields({});
expect(encoded).toEqual({});
});
it('should encode nested objects without modification', () => {
const event = {
event_id: 'abc123',
user: { id: 'user1', name: 'Test' },
contexts: { trace: { trace_id: 'xyz' } },
};
const encoded = encodeFields(event);
expect(encoded.eid).toBe('abc123');
// user is encoded to 'u', contexts is encoded to 'ctx'
expect(encoded.u).toEqual({ id: 'user1', name: 'Test' });
expect(encoded.ctx).toEqual({ trace: { trace_id: 'xyz' } });
});
it('should encode transaction fields', () => {
const transaction = {
type: 'transaction',
transaction: 'test-transaction',
duration: 1000,
spans: [],
start_timestamp: '2024-01-01T00:00:00.000Z',
};
const encoded = encodeFields(transaction);
expect(encoded.t).toBe('transaction');
expect(encoded.tx).toBe('test-transaction');
expect(encoded.d).toBe(1000);
expect(encoded.sp).toEqual([]);
expect(encoded.sts).toBe('2024-01-01T00:00:00.000Z');
});
});
describe('decodeFields', () => {
it('should decode short codes back to original field names', () => {
const event = {
eid: 'abc123',
ts: '2024-01-01T00:00:00.000Z',
t: 'error',
l: 'error',
m: 'Test error',
};
const decoded = decodeFields(event);
expect(decoded.event_id).toBe('abc123');
expect(decoded.timestamp).toBe('2024-01-01T00:00:00.000Z');
expect(decoded.type).toBe('error');
expect(decoded.level).toBe('error');
expect(decoded.message).toBe('Test error');
});
it('should keep unknown fields unchanged', () => {
const event = {
eid: 'abc123',
custom_field: 'custom_value',
another_field: 123,
};
const decoded = decodeFields(event);
expect(decoded.event_id).toBe('abc123');
expect(decoded.custom_field).toBe('custom_value');
expect(decoded.another_field).toBe(123);
});
it('should return non-object input unchanged', () => {
expect(decodeFields(null as any)).toBe(null);
expect(decodeFields(undefined as any)).toBe(undefined);
expect(decodeFields(123 as any)).toBe(123);
expect(decodeFields('string' as any)).toBe('string');
});
it('should handle empty object', () => {
const decoded = decodeFields({});
expect(decoded).toEqual({});
});
it('should decode nested objects without modification', () => {
const event = {
eid: 'abc123',
u: { id: 'user1', name: 'Test' },
ctx: { trace: { trace_id: 'xyz' } },
};
const decoded = decodeFields(event);
expect(decoded.event_id).toBe('abc123');
expect(decoded.user).toEqual({ id: 'user1', name: 'Test' });
expect(decoded.contexts).toEqual({ trace: { trace_id: 'xyz' } });
});
});
describe('encode/decode round-trip', () => {
it('should preserve data through encode/decode cycle', () => {
const original = {
event_id: 'abc123',
timestamp: '2024-01-01T00:00:00.000Z',
type: 'error',
level: 'error',
message: 'Test error',
user: { id: 'user1' },
tags: { env: 'prod' },
};
const encoded = encodeFields(original);
const decoded = decodeFields(encoded);
expect(decoded.event_id).toBe(original.event_id);
expect(decoded.timestamp).toBe(original.timestamp);
expect(decoded.type).toBe(original.type);
expect(decoded.level).toBe(original.level);
expect(decoded.message).toBe(original.message);
expect(decoded.user).toEqual(original.user);
expect(decoded.tags).toEqual(original.tags);
});
});
});

178
tests/helper.test.ts Normal file
View File

@ -0,0 +1,178 @@
import { describe, it, expect } from 'vitest';
import {
generateEventId,
now,
uuid,
isFunction,
isString,
isObject,
safeGet,
truncate,
matchPatterns,
} from '../src/utils/helper';
describe('helper utils', () => {
describe('generateEventId', () => {
it('should generate 32 character hex string', () => {
const id = generateEventId();
expect(id).toMatch(/^[a-f0-9]{32}$/i);
});
it('should generate unique IDs', () => {
const id1 = generateEventId();
const id2 = generateEventId();
expect(id1).not.toBe(id2);
});
});
describe('now', () => {
it('should return current timestamp in milliseconds', () => {
const before = Date.now();
const timestamp = now();
const after = Date.now();
expect(timestamp).toBeGreaterThanOrEqual(before);
expect(timestamp).toBeLessThanOrEqual(after);
});
});
describe('uuid', () => {
it('should generate 32 character hex string (UUID without hyphens)', () => {
const id = uuid();
expect(id).toMatch(/^[a-f0-9]{32}$/i);
});
it('should generate unique UUIDs', () => {
const id1 = uuid();
const id2 = uuid();
expect(id1).not.toBe(id2);
});
});
describe('isFunction', () => {
it('should return true for functions', () => {
expect(isFunction(() => {})).toBe(true);
expect(isFunction(async () => {})).toBe(true);
expect(isFunction(function() {})).toBe(true);
});
it('should return false for non-functions', () => {
expect(isFunction('string')).toBe(false);
expect(isFunction(123)).toBe(false);
expect(isFunction(null)).toBe(false);
expect(isFunction(undefined)).toBe(false);
expect(isFunction({})).toBe(false);
expect(isFunction([])).toBe(false);
});
});
describe('isString', () => {
it('should return true for strings', () => {
expect(isString('')).toBe(true);
expect(isString('hello')).toBe(true);
expect(isString(String(123))).toBe(true);
});
it('should return false for non-strings', () => {
expect(isString(123)).toBe(false);
expect(isString(null)).toBe(false);
expect(isString(undefined)).toBe(false);
expect(isString({})).toBe(false);
expect(isString([])).toBe(false);
});
});
describe('isObject', () => {
it('should return true for objects', () => {
expect(isObject({})).toBe(true);
expect(isObject({ a: 1 })).toBe(true);
expect(isObject(new Object())).toBe(true);
});
it('should return false for non-objects', () => {
expect(isObject(null)).toBe(false);
expect(isObject([])).toBe(false);
expect(isObject('string')).toBe(false);
expect(isObject(123)).toBe(false);
expect(isObject(undefined)).toBe(false);
expect(isObject(() => {})).toBe(false);
});
});
describe('safeGet', () => {
it('should return result of function when successful', () => {
const result = safeGet(() => 42, 0);
expect(result).toBe(42);
});
it('should return default value when function throws', () => {
const result = safeGet(() => { throw new Error('test'); }, 'default');
expect(result).toBe('default');
});
it('should return default value when function returns undefined', () => {
const result = safeGet(() => undefined, 'default');
expect(result).toBe('default');
});
it('should return null when function returns null (null is a valid value)', () => {
const result = safeGet(() => null, 'default');
expect(result).toBeNull();
});
it('should allow falsy values that are not undefined', () => {
expect(safeGet(() => 0, 100)).toBe(0);
expect(safeGet(() => '', 'default')).toBe('');
expect(safeGet(() => false, true)).toBe(false);
});
});
describe('truncate', () => {
it('should return original string if shorter than max length', () => {
expect(truncate('hello', 10)).toBe('hello');
expect(truncate('hello', 5)).toBe('hello');
});
it('should truncate string longer than max length', () => {
expect(truncate('hello world', 5)).toBe('hello...');
});
it('should handle empty string', () => {
expect(truncate('', 10)).toBe('');
});
it('should handle exact length', () => {
expect(truncate('hello', 5)).toBe('hello');
});
});
describe('matchPatterns', () => {
it('should return false for empty patterns', () => {
expect(matchPatterns('test', [])).toBe(false);
expect(matchPatterns('test', undefined as any)).toBe(false);
});
it('should match string patterns', () => {
expect(matchPatterns('error: something went wrong', ['error'])).toBe(true);
expect(matchPatterns('something went wrong', ['error'])).toBe(false);
});
it('should match multiple string patterns (OR)', () => {
expect(matchPatterns('warning: low memory', ['error', 'warning'])).toBe(true);
expect(matchPatterns('info: normal', ['error', 'warning'])).toBe(false);
});
it('should match RegExp patterns', () => {
expect(matchPatterns('Error: validation failed', [/Error/])).toBe(true);
expect(matchPatterns('error: validation failed', [/validation/])).toBe(true);
});
it('should match combined string and RegExp patterns', () => {
expect(matchPatterns('WARNING: high CPU', ['error', /WARNING/])).toBe(true);
});
it('should handle special regex characters in string patterns', () => {
expect(matchPatterns('test.file.txt', ['test.file'])).toBe(true);
expect(matchPatterns('test123', ['test(123)'])).toBe(false);
});
});
});

168
tests/identity.test.ts Normal file
View File

@ -0,0 +1,168 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
getAnonymousId,
setAnonymousId,
clearAnonymousId,
isAnonymousTrackingEnabled,
} from '../src/utils/identity';
const ANONYMOUS_ID_KEY = 'light_sentry_aid';
describe('identity utils', () => {
beforeEach(() => {
localStorage.clear();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('getAnonymousId', () => {
it('should generate and store a new anonymous ID when none exists', () => {
const id = getAnonymousId();
expect(id).toBeTruthy();
expect(typeof id).toBe('string');
expect(id!.length).toBeGreaterThan(0);
const stored = localStorage.getItem(ANONYMOUS_ID_KEY);
expect(stored).toBe(id);
});
it('should return the same ID on subsequent calls', () => {
const id1 = getAnonymousId();
const id2 = getAnonymousId();
expect(id1).toBe(id2);
});
it('should return existing ID from localStorage', () => {
localStorage.setItem(ANONYMOUS_ID_KEY, 'existing-id-123');
const id = getAnonymousId();
expect(id).toBe('existing-id-123');
});
it('should return null when localStorage is not available', () => {
const originalLocalStorage = (global as any).localStorage;
delete (global as any).localStorage;
const id = getAnonymousId();
expect(id).toBeNull();
(global as any).localStorage = originalLocalStorage;
});
it('should return null when localStorage throws', () => {
const getItemSpy = vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
throw new Error('Storage error');
});
const id = getAnonymousId();
expect(id).toBeNull();
getItemSpy.mockRestore();
});
it('should generate UUID format (32 hex chars without hyphens)', () => {
const id = getAnonymousId();
expect(id).toMatch(/^[a-f0-9]{32}$/i);
});
});
describe('setAnonymousId', () => {
it('should set custom anonymous ID', () => {
const result = setAnonymousId('custom-id-123');
expect(result).toBe(true);
expect(localStorage.getItem(ANONYMOUS_ID_KEY)).toBe('custom-id-123');
});
it('should override existing ID', () => {
localStorage.setItem(ANONYMOUS_ID_KEY, 'old-id');
setAnonymousId('new-id');
expect(localStorage.getItem(ANONYMOUS_ID_KEY)).toBe('new-id');
});
it('should return false when localStorage is not available', () => {
const originalLocalStorage = (global as any).localStorage;
delete (global as any).localStorage;
const result = setAnonymousId('test-id');
expect(result).toBe(false);
(global as any).localStorage = originalLocalStorage;
});
it('should return false when localStorage throws', () => {
const setItemSpy = vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
throw new Error('Storage error');
});
const result = setAnonymousId('test-id');
expect(result).toBe(false);
setItemSpy.mockRestore();
});
it('should set empty string ID', () => {
const result = setAnonymousId('');
expect(result).toBe(true);
expect(localStorage.getItem(ANONYMOUS_ID_KEY)).toBe('');
});
});
describe('clearAnonymousId', () => {
it('should clear existing anonymous ID', () => {
localStorage.setItem(ANONYMOUS_ID_KEY, 'existing-id');
const result = clearAnonymousId();
expect(result).toBe(true);
expect(localStorage.getItem(ANONYMOUS_ID_KEY)).toBeNull();
});
it('should return true even when no ID exists', () => {
const result = clearAnonymousId();
expect(result).toBe(true);
});
it('should return false when localStorage is not available', () => {
const originalLocalStorage = (global as any).localStorage;
delete (global as any).localStorage;
const result = clearAnonymousId();
expect(result).toBe(false);
(global as any).localStorage = originalLocalStorage;
});
it('should return false when localStorage throws', () => {
const removeItemSpy = vi.spyOn(Storage.prototype, 'removeItem').mockImplementation(() => {
throw new Error('Storage error');
});
const result = clearAnonymousId();
expect(result).toBe(false);
removeItemSpy.mockRestore();
});
});
describe('isAnonymousTrackingEnabled', () => {
it('should return true when trackAnonymousUsers is undefined', () => {
expect(isAnonymousTrackingEnabled({})).toBe(true);
});
it('should return true when trackAnonymousUsers is true', () => {
expect(isAnonymousTrackingEnabled({ trackAnonymousUsers: true })).toBe(true);
});
it('should return false when trackAnonymousUsers is false', () => {
expect(isAnonymousTrackingEnabled({ trackAnonymousUsers: false })).toBe(false);
});
it('should return true for other truthy values', () => {
expect(isAnonymousTrackingEnabled({ trackAnonymousUsers: 1 as any })).toBe(true);
expect(isAnonymousTrackingEnabled({ trackAnonymousUsers: 'yes' as any })).toBe(true);
});
});
describe('integration: full lifecycle', () => {
it('should handle set -> get -> clear flow', () => {
const id1 = getAnonymousId();
expect(id1).toBeTruthy();
setAnonymousId('custom-user-id');
const id2 = getAnonymousId();
expect(id2).toBe('custom-user-id');
clearAnonymousId();
const id3 = getAnonymousId();
expect(id3).toBeTruthy();
expect(id3).not.toBe('custom-user-id');
expect(id3).not.toBe(id1);
});
});
});

199
tests/index.test.ts Normal file
View File

@ -0,0 +1,199 @@
import { describe, it, expect } from 'vitest';
import * as exports from '../src/index';
describe('index exports', () => {
describe('named exports', () => {
it('should export init function', () => {
expect(typeof exports.init).toBe('function');
});
it('should export getClient function', () => {
expect(typeof exports.getClient).toBe('function');
});
it('should export captureException function', () => {
expect(typeof exports.captureException).toBe('function');
});
it('should export captureMessage function', () => {
expect(typeof exports.captureMessage).toBe('function');
});
it('should export captureEvent function', () => {
expect(typeof exports.captureEvent).toBe('function');
});
it('should export setUser function', () => {
expect(typeof exports.setUser).toBe('function');
});
it('should export setAnonymousId function', () => {
expect(typeof exports.setAnonymousId).toBe('function');
});
it('should export clearAnonymousId function', () => {
expect(typeof exports.clearAnonymousId).toBe('function');
});
it('should export getAnonymousId function', () => {
expect(typeof exports.getAnonymousId).toBe('function');
});
it('should export startTransaction function', () => {
expect(typeof exports.startTransaction).toBe('function');
});
it('should export getCurrentTransaction function', () => {
expect(typeof exports.getCurrentTransaction).toBe('function');
});
it('should export getCurrentSpan function', () => {
expect(typeof exports.getCurrentSpan).toBe('function');
});
it('should export setSpan function', () => {
expect(typeof exports.setSpan).toBe('function');
});
it('should export startSpan function', () => {
expect(typeof exports.startSpan).toBe('function');
});
it('should export setTag function', () => {
expect(typeof exports.setTag).toBe('function');
});
it('should export setTags function', () => {
expect(typeof exports.setTags).toBe('function');
});
it('should export setExtra function', () => {
expect(typeof exports.setExtra).toBe('function');
});
it('should export addBreadcrumb function', () => {
expect(typeof exports.addBreadcrumb).toBe('function');
});
it('should export flush function', () => {
expect(typeof exports.flush).toBe('function');
});
it('should export disable function', () => {
expect(typeof exports.disable).toBe('function');
});
it('should export enable function', () => {
expect(typeof exports.enable).toBe('function');
});
it('should export Client class', () => {
expect(typeof exports.Client).toBe('function');
});
it('should export Span class', () => {
expect(typeof exports.Span).toBe('function');
});
it('should export Transaction class', () => {
expect(typeof exports.Transaction).toBe('function');
});
it('should export httpStatusToSpanStatus function', () => {
expect(typeof exports.httpStatusToSpanStatus).toBe('function');
});
it('should export dbStatusToSpanStatus function', () => {
expect(typeof exports.dbStatusToSpanStatus).toBe('function');
});
it('should export statusToSpanStatus function', () => {
expect(typeof exports.statusToSpanStatus).toBe('function');
});
it('should export serializeBaggage function', () => {
expect(typeof exports.serializeBaggage).toBe('function');
});
it('should export parseBaggage function', () => {
expect(typeof exports.parseBaggage).toBe('function');
});
it('should export createBaggageFromTransaction function', () => {
expect(typeof exports.createBaggageFromTransaction).toBe('function');
});
});
describe('plugin exports', () => {
it('should export ErrorPlugin class', () => {
expect(typeof exports.ErrorPlugin).toBe('function');
});
it('should export PerformancePlugin class', () => {
expect(typeof exports.PerformancePlugin).toBe('function');
});
it('should export NetworkPlugin class', () => {
expect(typeof exports.NetworkPlugin).toBe('function');
});
it('should export BehaviorPlugin class', () => {
expect(typeof exports.BehaviorPlugin).toBe('function');
});
it('should export OfflinePlugin class', () => {
expect(typeof exports.OfflinePlugin).toBe('function');
});
it('should export SamplingPlugin class', () => {
expect(typeof exports.SamplingPlugin).toBe('function');
});
it('should export RouterPlugin class', () => {
expect(typeof exports.RouterPlugin).toBe('function');
});
it('should export InteractionPlugin class', () => {
expect(typeof exports.InteractionPlugin).toBe('function');
});
});
describe('default export', () => {
it('should export default object with core functions', () => {
const d = exports.default;
expect(d).toBeDefined();
expect(typeof d.init).toBe('function');
expect(typeof d.getClient).toBe('function');
expect(typeof d.captureException).toBe('function');
expect(typeof d.captureMessage).toBe('function');
expect(typeof d.captureEvent).toBe('function');
expect(typeof d.setUser).toBe('function');
expect(typeof d.setAnonymousId).toBe('function');
expect(typeof d.clearAnonymousId).toBe('function');
expect(typeof d.getAnonymousId).toBe('function');
expect(typeof d.startTransaction).toBe('function');
expect(typeof d.getCurrentTransaction).toBe('function');
expect(typeof d.getCurrentSpan).toBe('function');
expect(typeof d.setSpan).toBe('function');
expect(typeof d.startSpan).toBe('function');
expect(typeof d.setTag).toBe('function');
expect(typeof d.setTags).toBe('function');
expect(typeof d.setExtra).toBe('function');
expect(typeof d.addBreadcrumb).toBe('function');
expect(typeof d.flush).toBe('function');
expect(typeof d.disable).toBe('function');
expect(typeof d.enable).toBe('function');
});
it('should export default object with Span and Transaction classes', () => {
const d = exports.default;
expect(typeof d.Span).toBe('function');
expect(typeof d.Transaction).toBe('function');
});
it('should export default object with utility functions', () => {
const d = exports.default;
expect(typeof d.httpStatusToSpanStatus).toBe('function');
});
});
});

192
tests/logger.test.ts Normal file
View File

@ -0,0 +1,192 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
setDebug,
isDebugEnabled,
logDebug,
logError,
} from '../src/utils/logger';
describe('logger', () => {
beforeEach(() => {
setDebug(false);
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('setDebug', () => {
it('should enable debug mode', () => {
expect(isDebugEnabled()).toBe(false);
setDebug(true);
expect(isDebugEnabled()).toBe(true);
});
it('should disable debug mode', () => {
setDebug(true);
expect(isDebugEnabled()).toBe(true);
setDebug(false);
expect(isDebugEnabled()).toBe(false);
});
});
describe('isDebugEnabled', () => {
it('should return current debug state', () => {
expect(isDebugEnabled()).toBe(false);
setDebug(true);
expect(isDebugEnabled()).toBe(true);
});
});
describe('logDebug', () => {
it('should not log when debug is disabled', () => {
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
logDebug('Test', 'This should not appear');
expect(consoleSpy).not.toHaveBeenCalled();
});
it('should log message when debug is enabled', () => {
setDebug(true);
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
logDebug('Test', 'This should appear');
expect(consoleSpy).toHaveBeenCalled();
});
it('should log message with data when debug is enabled', () => {
setDebug(true);
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
logDebug('Test', 'Message with data', { key: 'value', num: 123 });
expect(consoleSpy).toHaveBeenCalled();
const loggedMessage = consoleSpy.mock.calls[0].join(' ');
expect(loggedMessage).toContain('[LightSDK][Test]');
expect(loggedMessage).toContain('Message with data');
});
it('should include timestamp in log message', () => {
setDebug(true);
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
logDebug('Module', 'Test message');
expect(consoleSpy).toHaveBeenCalled();
const loggedMessage = consoleSpy.mock.calls[0][0];
expect(loggedMessage).toMatch(/^\[LightSDK\]\[Module\]/);
});
it('should handle message without data', () => {
setDebug(true);
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
logDebug('Module', 'Simple message');
expect(consoleSpy).toHaveBeenCalled();
});
it('should handle undefined data', () => {
setDebug(true);
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
logDebug('Module', 'Message', undefined);
expect(consoleSpy).toHaveBeenCalled();
});
it('should handle null data', () => {
setDebug(true);
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
logDebug('Module', 'Message', null);
expect(consoleSpy).toHaveBeenCalled();
});
it('should handle complex data objects', () => {
setDebug(true);
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const complexData = {
nested: { deeply: { value: 123 } },
array: [1, 2, 3],
bool: true,
null: null,
};
logDebug('Module', 'Complex data', complexData);
expect(consoleSpy).toHaveBeenCalled();
});
});
describe('logError', () => {
it('should not log when debug is disabled', () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
logError('Test', 'This should not appear');
expect(consoleSpy).not.toHaveBeenCalled();
});
it('should log error message when debug is enabled', () => {
setDebug(true);
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
logError('Test', 'This error should appear');
expect(consoleSpy).toHaveBeenCalled();
});
it('should include ERROR tag in log message', () => {
setDebug(true);
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
logError('Module', 'Error message');
expect(consoleSpy).toHaveBeenCalled();
const loggedMessage = consoleSpy.mock.calls[0][0];
expect(loggedMessage).toContain('[LightSDK][Module][ERROR]');
});
it('should log error with error data', () => {
setDebug(true);
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const error = new Error('Test error');
logError('Module', 'Error occurred', error);
expect(consoleSpy).toHaveBeenCalled();
});
it('should handle plain object error', () => {
setDebug(true);
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const errorObj = { status: 404, message: 'Not found' };
logError('Module', 'Request failed', errorObj);
expect(consoleSpy).toHaveBeenCalled();
});
it('should handle error message without data', () => {
setDebug(true);
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
logError('Module', 'Simple error');
expect(consoleSpy).toHaveBeenCalled();
});
it('should handle string error', () => {
setDebug(true);
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
logError('Module', 'String error', 'error details');
expect(consoleSpy).toHaveBeenCalled();
});
it('should handle undefined error', () => {
setDebug(true);
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
logError('Module', 'Error without details', undefined);
expect(consoleSpy).toHaveBeenCalled();
});
});
describe('debug toggle behavior', () => {
it('should toggle between enabled and disabled', () => {
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
setDebug(true);
logDebug('Test', 'Debug on');
expect(consoleLogSpy).toHaveBeenCalled();
setDebug(false);
logDebug('Test', 'Debug off');
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
setDebug(true);
logError('Test', 'Error on');
expect(consoleErrorSpy).toHaveBeenCalled();
setDebug(false);
logError('Test', 'Error off');
expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
});
});
});

360
tests/stacktrace.test.ts Normal file
View File

@ -0,0 +1,360 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { parseStackTrace } from '../src/utils/stacktrace';
describe('stacktrace', () => {
// Mock location for jsdom environment
beforeEach(() => {
delete (global as any).location;
});
describe('parseStackTrace', () => {
it('should return empty array for undefined stack', () => {
expect(parseStackTrace(undefined)).toEqual([]);
});
it('should return empty array for empty string', () => {
expect(parseStackTrace('')).toEqual([]);
});
it('should parse standard Chrome/Node.js stack trace format', () => {
// Stack trace: newest call first (Function1), then Function2, then Function3
// After reverse, order becomes Function3, Function2, Function1
const stack = `Error
at Function3 (http://example.com/js/index.js:30:5)
at Function2 (http://example.com/js/main.js:25:10)
at Function1 (http://example.com/js/app.js:10:15)`;
const frames = parseStackTrace(stack);
expect(frames).toHaveLength(3);
// After reverse, Function1 should be first (oldest call in the trace)
expect(frames[0]).toEqual({
filename: 'http://example.com/js/app.js',
function: 'Function1',
lineno: 10,
colno: 15,
in_app: true,
});
expect(frames[1]).toEqual({
filename: 'http://example.com/js/main.js',
function: 'Function2',
lineno: 25,
colno: 10,
in_app: true,
});
expect(frames[2]).toEqual({
filename: 'http://example.com/js/index.js',
function: 'Function3',
lineno: 30,
colno: 5,
in_app: true,
});
});
it('should parse V8 format with @ separator', () => {
// V8 format requires 'at' prefix: at Function1@path:line:col
const stack = `Error
at Function1@http://example.com/js/app.js:10:15
at Function2@http://example.com/js/main.js:25:10`;
const frames = parseStackTrace(stack);
expect(frames).toHaveLength(2);
// After reverse, order is reversed
expect(frames[0]).toEqual({
filename: 'http://example.com/js/main.js',
function: 'Function2',
lineno: 25,
colno: 10,
in_app: true,
});
expect(frames[1]).toEqual({
filename: 'http://example.com/js/app.js',
function: 'Function1',
lineno: 10,
colno: 15,
in_app: true,
});
});
it('should parse URL-only format (no function name)', () => {
const stack = `Error
at http://example.com/js/app.js:10:15
at http://example.com/js/main.js:25:10`;
const frames = parseStackTrace(stack);
expect(frames).toHaveLength(2);
expect(frames[0]).toEqual({
filename: 'http://example.com/js/main.js',
lineno: 25,
colno: 10,
in_app: true,
});
expect(frames[1]).toEqual({
filename: 'http://example.com/js/app.js',
lineno: 10,
colno: 15,
in_app: true,
});
});
it('should mark node_modules frames as in_app: false when captured', () => {
// When captureNodeModules is true, node_modules frames are included
const stack = `Error
at Function1 (http://example.com/node_modules/dep.js:10:15)
at Function2 (http://example.com/js/app.js:25:10)`;
const frames = parseStackTrace(stack, { captureNodeModules: true });
// After reverse, order is Function2, Function1
expect(frames).toHaveLength(2);
expect(frames[0].in_app).toBe(true); // Function2 (app code)
expect(frames[1].in_app).toBe(false); // Function1 (node_modules)
});
it('should skip node_modules frames when captureNodeModules is false', () => {
const stack = `Error
at Function1 (http://example.com/node_modules/dep.js:10:15)
at Function2 (http://example.com/js/app.js:25:10)
at Function3 (http://example.com/node_modules/other.js:30:5)`;
const frames = parseStackTrace(stack, { captureNodeModules: false });
// After reverse: Function3, Function2, Function1, then filter node_modules
expect(frames).toHaveLength(1);
expect(frames[0].function).toBe('Function2');
});
it('should include node_modules when captureNodeModules is true', () => {
const stack = `Error
at Function1 (http://example.com/node_modules/dep.js:10:15)
at Function2 (http://example.com/js/app.js:25:10)`;
const frames = parseStackTrace(stack, { captureNodeModules: true });
expect(frames).toHaveLength(2);
});
it('should not include column when captureColumn is false', () => {
const stack = `Error
at Function1 (http://example.com/js/app.js:10:15)`;
const frames = parseStackTrace(stack, { captureColumn: false });
expect(frames).toHaveLength(1);
expect(frames[0].colno).toBeUndefined();
});
it('should not apply relativePathOnly when origin does not match', () => {
// In jsdom, location.origin would be 'http://localhost:3000' or similar
// so example.com URLs won't match
const stack = `Error
at Function2 (http://other.com/js/main.js:25:10)
at Function1 (http://example.com/js/app.js:10:15)`;
const frames = parseStackTrace(stack, { relativePathOnly: true });
// URLs don't match location.origin, so they remain unchanged
// After reverse: Function1, Function2
expect(frames[0].filename).toBe('http://example.com/js/app.js');
expect(frames[1].filename).toBe('http://other.com/js/main.js');
});
it('should limit frames with maxFrames option', () => {
const stack = `Error
at Function4 (http://example.com/js/index.js:40:0)
at Function3 (http://example.com/js/util.js:35:0)
at Function2 (http://example.com/js/main.js:25:10)
at Function1 (http://example.com/js/app.js:10:15)`;
// After reverse: Function1, Function2, Function3, Function4
// maxFrames=2 returns first 2: Function1, Function2
const frames = parseStackTrace(stack, { maxFrames: 2 });
expect(frames).toHaveLength(2);
expect(frames[0].function).toBe('Function1');
expect(frames[1].function).toBe('Function2');
});
it('should return all frames when maxFrames is 0 or undefined', () => {
const stack = `Error
at Function1 (http://example.com/js/app.js:10:15)
at Function2 (http://example.com/js/main.js:25:10)`;
const frames1 = parseStackTrace(stack, { maxFrames: 0 });
const frames2 = parseStackTrace(stack, { maxFrames: undefined });
expect(frames1).toHaveLength(2);
expect(frames2).toHaveLength(2);
});
it('should handle anonymous functions in V8 format', () => {
// V8 format requires 'at' prefix: at <anonymous>@path:line:col
const stack = `Error
at <anonymous>@http://example.com/js/app.js:10:15`;
const frames = parseStackTrace(stack);
expect(frames).toHaveLength(1);
expect(frames[0].function).toBe('<anonymous>');
});
it('should handle stack with extra whitespace', () => {
// Note: complex whitespace may cause regex to fail, test with standard format
const stack = `Error
at Function2 (http://example.com/js/main.js:25:10)
at Function1 (http://example.com/js/app.js:10:15)`;
const frames = parseStackTrace(stack);
// After reverse: Function1, Function2
expect(frames).toHaveLength(2);
expect(frames[0].function).toBe('Function1');
expect(frames[1].function).toBe('Function2');
});
it('should handle file:// URLs', () => {
const stack = `Error
at Function1 (file:///Users/name/project/app.js:10:15)`;
const frames = parseStackTrace(stack);
expect(frames).toHaveLength(1);
expect(frames[0].filename).toBe('file:///Users/name/project/app.js');
expect(frames[0].in_app).toBe(true);
});
it('should handle mixed format stacks', () => {
const stack = `Error
at Function1 (http://example.com/js/app.js:10:15)
at Function2@http://example.com/js/main.js:25:10
at http://example.com/js/index.js:30:5`;
const frames = parseStackTrace(stack);
expect(frames).toHaveLength(3);
});
it('should handle V8 format with captureColumn: false', () => {
const stack = `Error
at Function1@http://example.com/js/app.js:10:15`;
const frames = parseStackTrace(stack, { captureColumn: false });
expect(frames).toHaveLength(1);
expect(frames[0].colno).toBeUndefined();
expect(frames[0].function).toBe('Function1');
});
it('should handle URL-only format with captureColumn: false', () => {
const stack = `Error
at http://example.com/js/app.js:10:15`;
const frames = parseStackTrace(stack, { captureColumn: false });
expect(frames).toHaveLength(1);
expect(frames[0].colno).toBeUndefined();
});
it('should skip node_modules in V8 format', () => {
const stack = `Error
at Function1@http://example.com/node_modules/dep.js:10:15
at Function2@http://example.com/js/app.js:25:10`;
const frames = parseStackTrace(stack, { captureNodeModules: false });
expect(frames).toHaveLength(1);
expect(frames[0].function).toBe('Function2');
});
it('should skip node_modules in URL-only format', () => {
const stack = `Error
at http://example.com/node_modules/dep.js:10:15
at http://example.com/js/app.js:25:10`;
const frames = parseStackTrace(stack, { captureNodeModules: false });
expect(frames).toHaveLength(1);
});
it('should include node_modules in V8 format when captureNodeModules is true', () => {
const stack = `Error
at Function1@http://example.com/node_modules/dep.js:10:15
at Function2@http://example.com/js/app.js:25:10`;
const frames = parseStackTrace(stack, { captureNodeModules: true });
expect(frames).toHaveLength(2);
expect(frames[0].in_app).toBe(true);
expect(frames[1].in_app).toBe(false);
});
it('should include node_modules in URL-only format when captureNodeModules is true', () => {
const stack = `Error
at http://example.com/node_modules/dep.js:10:15
at http://example.com/js/app.js:25:10`;
const frames = parseStackTrace(stack, { captureNodeModules: true });
expect(frames).toHaveLength(2);
});
it('should apply relativePathOnly to V8 format', () => {
// Set up location mock
const mockLocation = {
protocol: 'http:',
host: 'example.com',
};
(global as any).location = mockLocation;
const stack = `Error
at Function1@http://example.com/js/app.js:10:15
at Function2@http://example.com/js/main.js:25:10`;
const frames = parseStackTrace(stack, { relativePathOnly: true });
expect(frames).toHaveLength(2);
expect(frames[0].filename).toBe('/js/main.js');
expect(frames[1].filename).toBe('/js/app.js');
delete (global as any).location;
});
it('should apply relativePathOnly to URL-only format', () => {
const mockLocation = {
protocol: 'http:',
host: 'example.com',
};
(global as any).location = mockLocation;
const stack = `Error
at http://example.com/js/app.js:10:15
at http://example.com/js/main.js:25:10`;
const frames = parseStackTrace(stack, { relativePathOnly: true });
expect(frames).toHaveLength(2);
expect(frames[0].filename).toBe('/js/main.js');
expect(frames[1].filename).toBe('/js/app.js');
delete (global as any).location;
});
it('should return reversed frames (newest call first after reversal)', () => {
// In standard stack traces, the first line after "Error" is the most recent call
const stack = `Error
at Function1 (http://example.com/js/app.js:10:15)
at Function2 (http://example.com/js/main.js:25:10)
at Function3 (http://example.com/js/index.js:30:5)`;
const frames = parseStackTrace(stack);
// After reverse of [Function1, Function2, Function3], we get [Function3, Function2, Function1]
// This puts the oldest call first
expect(frames[0].function).toBe('Function3');
expect(frames[1].function).toBe('Function2');
expect(frames[2].function).toBe('Function1');
});
});
});

458
tests/tracing.test.ts Normal file
View File

@ -0,0 +1,458 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Span, Transaction, httpStatusToSpanStatus, dbStatusToSpanStatus, statusToSpanStatus } from '../src/utils/tracing';
import type { SpanContext, TransactionContext } from '../src/types';
describe('Span', () => {
let span: Span;
beforeEach(() => {
span = new Span({ op: 'test.span' });
});
describe('constructor', () => {
it('should create span with generated IDs when not provided', () => {
const newSpan = new Span({ op: 'test' });
expect(newSpan.traceId).toMatch(/^[a-f0-9]{32}$/i);
expect(newSpan.spanId).toMatch(/^[a-f0-9]{16}$/i);
});
it('should use provided traceId and spanId', () => {
const newSpan = new Span({
op: 'test',
traceId: 'abc123abc123abc123abc123abc123ab',
spanId: 'abc123abc123abcd',
});
expect(newSpan.traceId).toBe('abc123abc123abc123abc123abc123ab');
expect(newSpan.spanId).toBe('abc123abc123abcd');
});
it('should set parentSpanId from context', () => {
const childSpan = new Span({
op: 'child',
parentSpanId: 'parent-span-id',
});
expect(childSpan.parentSpanId).toBe('parent-span-id');
});
it('should set op and description', () => {
const newSpan = new Span({
op: 'http.client',
description: 'GET /api/users',
});
expect(newSpan.op).toBe('http.client');
expect(newSpan.description).toBe('GET /api/users');
});
it('should initialize with current timestamp', () => {
const before = Date.now();
const newSpan = new Span({ op: 'test' });
const after = Date.now();
expect(newSpan.startTimestamp).toBeGreaterThanOrEqual(before);
expect(newSpan.startTimestamp).toBeLessThanOrEqual(after);
});
it('should use provided startTimestamp', () => {
const newSpan = new Span({
op: 'test',
startTimestamp: 1234567890000,
});
expect(newSpan.startTimestamp).toBe(1234567890000);
});
});
describe('setTag', () => {
it('should add tag to span', () => {
const result = span.setTag('http.method', 'GET');
expect(span.tags).toEqual({ 'http.method': 'GET' });
expect(result).toBe(span); // should return this for chaining
});
it('should overwrite existing tag', () => {
span.setTag('http.method', 'GET');
span.setTag('http.method', 'POST');
expect(span.tags['http.method']).toBe('POST');
});
it('should support chaining', () => {
const result = span.setTag('key1', 'value1').setTag('key2', 'value2');
expect(result).toBe(span);
expect(span.tags).toEqual({ key1: 'value1', key2: 'value2' });
});
});
describe('setData', () => {
it('should add data to span', () => {
span.setData('http.url', 'https://api.example.com/users');
expect(span.data).toEqual({ 'http.url': 'https://api.example.com/users' });
});
it('should support chaining', () => {
const result = span.setData('key1', 'value1').setData('key2', 'value2');
expect(result).toBe(span);
});
});
describe('setStatus', () => {
it('should set status', () => {
span.setStatus('ok');
expect(span.status).toBe('ok');
});
it('should support chaining', () => {
const result = span.setStatus('ok');
expect(result).toBe(span);
});
});
describe('setHttpStatus', () => {
it('should set status for 2xx', () => {
span.setHttpStatus(200);
expect(span.status).toBe('ok');
expect(span.tags['http.status_code']).toBe('200');
});
it('should set status for 4xx', () => {
span.setHttpStatus(404);
expect(span.status).toBe('not_found');
expect(span.tags['http.status_code']).toBe('404');
});
it('should set status for 5xx', () => {
span.setHttpStatus(500);
expect(span.status).toBe('internal_error');
expect(span.tags['http.status_code']).toBe('500');
});
it('should support chaining', () => {
const result = span.setHttpStatus(200);
expect(result).toBe(span);
});
});
describe('setStatusFromResponse', () => {
it('should handle string status', () => {
span.setStatusFromResponse('ok');
expect(span.status).toBe('ok');
});
it('should handle number status', () => {
span.setStatusFromResponse(404);
expect(span.status).toBe('not_found');
});
it('should handle undefined status', () => {
span.setStatusFromResponse(undefined);
expect(span.status).toBe('unknown_error');
});
it('should set tag when tagKey provided', () => {
span.setStatusFromResponse(200, 'custom.status');
expect(span.tags['custom.status']).toBe('200');
});
});
describe('startChild', () => {
it('should create child span', () => {
const child = span.startChild({ op: 'http.client' });
expect(child).toBeDefined();
expect(child.op).toBe('http.client');
expect(child.parentSpanId).toBe(span.spanId);
expect(child.traceId).toBe(span.traceId);
});
it('should set transaction reference', () => {
const tx = new Transaction({ name: 'test-tx' });
const child = tx.startChild({ op: 'child' });
expect(child.transaction).toBe(tx);
});
it('should not add span to unsampled transaction', () => {
const tx = new Transaction({ name: 'test-tx', sampled: false });
const child = tx.startChild({ op: 'child' });
expect(tx.spans.length).toBe(0);
});
});
describe('finish', () => {
it('should set endTimestamp', () => {
span.finish();
expect(span.endTimestamp).toBeDefined();
});
it('should use provided endTimestamp', () => {
span.finish(1234567890000);
expect(span.endTimestamp).toBe(1234567890000);
});
it('should only finish once', () => {
span.finish();
const firstEndTime = span.endTimestamp;
span.finish(); // should not change
expect(span.endTimestamp).toBe(firstEndTime);
});
});
describe('toTraceparent', () => {
it('should return traceparent without sampled flag when not sampled', () => {
const newSpan = new Span({ op: 'test' });
const traceparent = newSpan.toTraceparent();
expect(traceparent).toMatch(/^[a-f0-9]{32}-[a-f0-9]{16}$/);
});
it('should return traceparent with sampled=1 when sampled', () => {
const tx = new Transaction({ name: 'test', sampled: true });
const traceparent = tx.toTraceparent();
expect(traceparent).toMatch(/^[a-f0-9]{32}-[a-f0-9]{16}-1$/);
});
it('should return traceparent with sampled=0 when not sampled', () => {
const tx = new Transaction({ name: 'test', sampled: false });
const traceparent = tx.toTraceparent();
expect(traceparent).toMatch(/^[a-f0-9]{32}-[a-f0-9]{16}-0$/);
});
});
describe('toJSON', () => {
it('should return correct JSON structure', () => {
span.setTag('http.method', 'GET');
span.setData('http.url', 'https://api.example.com');
span.setStatus('ok');
span.finish();
const json = span.toJSON();
expect(json.trace_id).toBe(span.traceId);
expect(json.span_id).toBe(span.spanId);
expect(json.op).toBe('test.span');
expect(json.status).toBe('ok');
expect(json.tags).toEqual({ 'http.method': 'GET' });
expect(json.data).toEqual({ 'http.url': 'https://api.example.com' });
expect(json.start_timestamp).toBe(span.startTimestamp);
expect(json.timestamp).toBe(span.endTimestamp);
});
it('should omit empty tags and data', () => {
const newSpan = new Span({ op: 'test' });
const json = newSpan.toJSON();
expect(json.tags).toBeUndefined();
expect(json.data).toBeUndefined();
});
});
});
describe('Transaction', () => {
let transaction: Transaction;
beforeEach(() => {
transaction = new Transaction({ name: 'test-transaction', op: 'transaction' });
});
describe('constructor', () => {
it('should create transaction with name and op', () => {
expect(transaction.name).toBe('test-transaction');
expect(transaction.op).toBe('transaction');
});
it('should set transaction reference to itself', () => {
expect(transaction.transaction).toBe(transaction);
});
it('should initialize empty spans array', () => {
expect(transaction.spans).toEqual([]);
});
it('should set maxSpans default', () => {
expect(transaction.maxSpans).toBe(1000);
});
it('should accept custom maxSpans', () => {
const tx = new Transaction({ name: 'test' }, undefined, undefined, undefined, undefined, 500);
expect(tx.maxSpans).toBe(500);
});
});
describe('_addSpan', () => {
it('should add span to transaction', () => {
const child = transaction.startChild({ op: 'child' });
expect(transaction.spans.length).toBe(1);
expect(transaction.spans[0]).toBe(child);
});
it('should not exceed maxSpans', () => {
const tx = new Transaction({ name: 'test' }, undefined, undefined, undefined, undefined, 2);
tx.startChild({ op: 'child1' });
tx.startChild({ op: 'child2' });
tx.startChild({ op: 'child3' }); // should be dropped
expect(tx.spans.length).toBe(2);
});
});
describe('setBaggage', () => {
it('should set baggage field', () => {
const result = transaction.setBaggage('custom.key', 'value');
expect(transaction.toBaggage()).toContain('custom.key=value');
expect(result).toBe(transaction);
});
});
describe('setUserBaggage', () => {
it('should set user_id in baggage', () => {
transaction.setUserBaggage('user123');
expect(transaction.toBaggage()).toContain('sentry-user_id=user123');
});
it('should set user_segment in baggage', () => {
transaction.setUserBaggage('user123', 'premium');
expect(transaction.toBaggage()).toContain('sentry-user_id=user123');
expect(transaction.toBaggage()).toContain('sentry-user_segment=premium');
});
it('should delete user_id when undefined', () => {
transaction.setUserBaggage('user123');
transaction.setUserBaggage(undefined);
expect(transaction.toBaggage()).not.toContain('user_id');
});
});
describe('toBaggage', () => {
it('should return serialized baggage with trace_id', () => {
const baggage = transaction.toBaggage();
expect(baggage).toContain('sentry-trace_id=');
});
it('should include sampled when explicitly set', () => {
const txWithSample = new Transaction({ name: 'test', sampled: true });
const baggage = txWithSample.toBaggage();
expect(baggage).toContain('sentry-sampled=true');
});
});
describe('toJSON', () => {
it('should include spans in JSON', () => {
transaction.startChild({ op: 'child1' });
transaction.startChild({ op: 'child2' });
const json = transaction.toJSON();
expect(json.spans).toHaveLength(2);
expect(json.transaction).toBe('test-transaction');
expect(json.sampled).toBeUndefined(); // not sampled by default
});
});
describe('toEvent', () => {
it('should return transaction event', () => {
transaction.startChild({ op: 'child' });
const event = transaction.toEvent();
expect(event.type).toBe('transaction');
expect(event.transaction).toBe('test-transaction');
expect(event.spans).toHaveLength(1);
expect(event.contexts?.trace).toBeDefined();
expect(event.platform).toBe('javascript');
});
});
});
describe('httpStatusToSpanStatus', () => {
it('should map 2xx to ok', () => {
expect(httpStatusToSpanStatus(200)).toBe('ok');
expect(httpStatusToSpanStatus(201)).toBe('ok');
expect(httpStatusToSpanStatus(299)).toBe('ok');
});
it('should map 400 to invalid_argument', () => {
expect(httpStatusToSpanStatus(400)).toBe('invalid_argument');
});
it('should map 401 to unauthenticated', () => {
expect(httpStatusToSpanStatus(401)).toBe('unauthenticated');
});
it('should map 403 to permission_denied', () => {
expect(httpStatusToSpanStatus(403)).toBe('permission_denied');
});
it('should map 404 to not_found', () => {
expect(httpStatusToSpanStatus(404)).toBe('not_found');
});
it('should map 409 to already_exists', () => {
expect(httpStatusToSpanStatus(409)).toBe('already_exists');
});
it('should map 429 to resource_exhausted', () => {
expect(httpStatusToSpanStatus(429)).toBe('resource_exhausted');
});
it('should map 4xx to invalid_argument', () => {
expect(httpStatusToSpanStatus(499)).toBe('invalid_argument');
});
it('should map 5xx to internal_error', () => {
expect(httpStatusToSpanStatus(500)).toBe('internal_error');
expect(httpStatusToSpanStatus(503)).toBe('internal_error');
expect(httpStatusToSpanStatus(599)).toBe('internal_error');
});
});
describe('dbStatusToSpanStatus', () => {
it('should map 0 and 1 to ok', () => {
expect(dbStatusToSpanStatus(0)).toBe('ok');
expect(dbStatusToSpanStatus(1)).toBe('ok');
expect(dbStatusToSpanStatus('0')).toBe('ok');
expect(dbStatusToSpanStatus('1')).toBe('ok');
});
it('should map 10-19 to invalid_argument', () => {
expect(dbStatusToSpanStatus(10)).toBe('invalid_argument');
expect(dbStatusToSpanStatus(15)).toBe('invalid_argument');
});
it('should map 20-29 to not_found', () => {
expect(dbStatusToSpanStatus(20)).toBe('not_found');
expect(dbStatusToSpanStatus(25)).toBe('not_found');
});
it('should map 30-39 to already_exists', () => {
expect(dbStatusToSpanStatus(30)).toBe('already_exists');
expect(dbStatusToSpanStatus(35)).toBe('already_exists');
});
it('should map 50+ to internal_error', () => {
expect(dbStatusToSpanStatus(50)).toBe('internal_error');
expect(dbStatusToSpanStatus(100)).toBe('internal_error');
});
it('should return unknown_error for undefined/null/empty', () => {
expect(dbStatusToSpanStatus(undefined)).toBe('unknown_error');
expect(dbStatusToSpanStatus(null)).toBe('unknown_error');
expect(dbStatusToSpanStatus('')).toBe('unknown_error');
});
});
describe('statusToSpanStatus', () => {
it('should handle string status ok', () => {
expect(statusToSpanStatus('ok', undefined)).toBe('ok');
expect(statusToSpanStatus('success', undefined)).toBe('ok');
});
it('should handle string status cancelled', () => {
expect(statusToSpanStatus('cancelled', undefined)).toBe('cancelled');
expect(statusToSpanStatus('canceled', undefined)).toBe('cancelled');
});
it('should handle HTTP status codes', () => {
expect(statusToSpanStatus(404, undefined)).toBe('not_found');
expect(statusToSpanStatus(500, undefined)).toBe('internal_error');
});
it('should use db rules for db ops', () => {
expect(statusToSpanStatus(0, 'db.query')).toBe('ok');
expect(statusToSpanStatus(10, 'db.query')).toBe('invalid_argument');
});
it('should return unknown_error for unknown strings', () => {
expect(statusToSpanStatus('unknown_status', undefined)).toBe('unknown_error');
});
});