@loadstrike/loadstrike-sdk 1.0.27101 → 1.0.29201
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/cjs/index.js +27 -2
- package/dist/cjs/local.js +71 -1
- package/dist/cjs/runtime.js +314 -2
- package/dist/cjs/sinks.js +262 -1
- package/dist/cjs/transports.js +326 -13
- package/dist/esm/index.js +3 -3
- package/dist/esm/local.js +71 -1
- package/dist/esm/runtime.js +308 -1
- package/dist/esm/sinks.js +249 -0
- package/dist/esm/transports.js +319 -12
- package/dist/types/contracts.d.ts +12 -0
- package/dist/types/index.d.ts +5 -5
- package/dist/types/runtime.d.ts +67 -0
- package/dist/types/sinks.d.ts +136 -0
- package/dist/types/transports.d.ts +114 -0
- package/package.json +1 -1
package/dist/esm/runtime.js
CHANGED
|
@@ -1606,6 +1606,169 @@ export class LoadStrikeContext {
|
|
|
1606
1606
|
}
|
|
1607
1607
|
const ACCESSIBILITY_TESTING_FEATURE = "testing.accessibility";
|
|
1608
1608
|
const BROWSER_WEB_VITALS_FEATURE = "testing.browser_web_vitals";
|
|
1609
|
+
export class LoadStrikeAccessibilityBaseline {
|
|
1610
|
+
constructor(initial = {}) {
|
|
1611
|
+
const source = qualityRecord(initial);
|
|
1612
|
+
this.allowedRuleIds = qualityStringArray(qualityValue(source, "allowedRuleIds", "AllowedRuleIds"));
|
|
1613
|
+
this.AllowedRuleIds = this.allowedRuleIds;
|
|
1614
|
+
this.maxNewViolations = Math.max(0, qualityNumber(source, 0, "maxNewViolations", "MaxNewViolations"));
|
|
1615
|
+
this.MaxNewViolations = this.maxNewViolations;
|
|
1616
|
+
}
|
|
1617
|
+
compare(result) {
|
|
1618
|
+
const violations = Array.isArray(result?.violations) ? result.violations : [];
|
|
1619
|
+
const allowed = new Set(this.allowedRuleIds.map((value) => value.toLowerCase()));
|
|
1620
|
+
const newViolations = violations.filter((violation) => !allowed.has(String(violation.ruleId ?? "").toLowerCase()));
|
|
1621
|
+
const newRuleIds = Array.from(new Set(newViolations
|
|
1622
|
+
.map((violation) => String(violation.ruleId ?? "").trim())
|
|
1623
|
+
.filter(Boolean)));
|
|
1624
|
+
return {
|
|
1625
|
+
passed: newViolations.length <= this.maxNewViolations,
|
|
1626
|
+
Passed: newViolations.length <= this.maxNewViolations,
|
|
1627
|
+
newViolationCount: newViolations.length,
|
|
1628
|
+
NewViolationCount: newViolations.length,
|
|
1629
|
+
newRuleIds,
|
|
1630
|
+
NewRuleIds: newRuleIds
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
Compare(result) {
|
|
1634
|
+
return this.compare(result);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
export class LoadStrikeAccessibilityCiGate {
|
|
1638
|
+
constructor(initial = {}) {
|
|
1639
|
+
const source = qualityRecord(initial);
|
|
1640
|
+
this.maxViolations = qualityOptionalNumber(source, "maxViolations", "MaxViolations");
|
|
1641
|
+
this.MaxViolations = this.maxViolations;
|
|
1642
|
+
this.maxCriticalViolations = qualityOptionalNumber(source, "maxCriticalViolations", "MaxCriticalViolations");
|
|
1643
|
+
this.MaxCriticalViolations = this.maxCriticalViolations;
|
|
1644
|
+
this.maxSeriousViolations = qualityOptionalNumber(source, "maxSeriousViolations", "MaxSeriousViolations");
|
|
1645
|
+
this.MaxSeriousViolations = this.maxSeriousViolations;
|
|
1646
|
+
const baseline = qualityValue(source, "baseline", "Baseline");
|
|
1647
|
+
this.baseline = baseline instanceof LoadStrikeAccessibilityBaseline
|
|
1648
|
+
? baseline
|
|
1649
|
+
: (baseline && typeof baseline === "object" ? new LoadStrikeAccessibilityBaseline(baseline) : undefined);
|
|
1650
|
+
this.Baseline = this.baseline;
|
|
1651
|
+
}
|
|
1652
|
+
evaluate(result) {
|
|
1653
|
+
const normalized = attachAccessibilityCounts(result);
|
|
1654
|
+
const reasons = [];
|
|
1655
|
+
if (this.maxViolations != null && (normalized.totalViolations ?? 0) > this.maxViolations) {
|
|
1656
|
+
reasons.push(`total accessibility violations ${normalized.totalViolations} exceeded ${this.maxViolations}.`);
|
|
1657
|
+
}
|
|
1658
|
+
if (this.maxCriticalViolations != null && (normalized.criticalViolations ?? 0) > this.maxCriticalViolations) {
|
|
1659
|
+
reasons.push(`critical accessibility violations ${normalized.criticalViolations} exceeded ${this.maxCriticalViolations}.`);
|
|
1660
|
+
}
|
|
1661
|
+
if (this.maxSeriousViolations != null && (normalized.seriousViolations ?? 0) > this.maxSeriousViolations) {
|
|
1662
|
+
reasons.push(`serious accessibility violations ${normalized.seriousViolations} exceeded ${this.maxSeriousViolations}.`);
|
|
1663
|
+
}
|
|
1664
|
+
if (this.baseline) {
|
|
1665
|
+
const baseline = this.baseline.compare(normalized);
|
|
1666
|
+
if (!baseline.passed) {
|
|
1667
|
+
reasons.push(`baseline found ${baseline.newViolationCount} new accessibility violation(s).`);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
return {
|
|
1671
|
+
passed: reasons.length === 0,
|
|
1672
|
+
Passed: reasons.length === 0,
|
|
1673
|
+
reasons,
|
|
1674
|
+
Reasons: reasons
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
Evaluate(result) {
|
|
1678
|
+
return this.evaluate(result);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
export class LoadStrikeAccessibilityScanner {
|
|
1682
|
+
static scanHtml(options, html) {
|
|
1683
|
+
validateAbsoluteUrl(options?.url, "Accessibility check URL");
|
|
1684
|
+
const source = String(html ?? "");
|
|
1685
|
+
const violations = [];
|
|
1686
|
+
if (!/<html\b[^>]*\blang\s*=\s*["'][^"']+["']/i.test(source)) {
|
|
1687
|
+
violations.push(createAccessibilityViolation("html-has-lang", "serious", "The html element should declare a non-empty language."));
|
|
1688
|
+
}
|
|
1689
|
+
if (/<title\b[^>]*>\s*<\/title>/i.test(source) || !/<title\b/i.test(source)) {
|
|
1690
|
+
violations.push(createAccessibilityViolation("document-title", "serious", "The page should provide a useful title."));
|
|
1691
|
+
}
|
|
1692
|
+
for (const match of source.matchAll(/<img\b(?![^>]*\balt\s*=)[^>]*>/gi)) {
|
|
1693
|
+
violations.push(createAccessibilityViolation("image-alt", "serious", "Images should provide alternate text.", match[0]));
|
|
1694
|
+
}
|
|
1695
|
+
for (const match of source.matchAll(/<button\b(?![^>]*\baria-label\s*=)[^>]*>\s*<\/button>/gi)) {
|
|
1696
|
+
violations.push(createAccessibilityViolation("button-name", "critical", "Buttons should have an accessible name.", match[0]));
|
|
1697
|
+
}
|
|
1698
|
+
for (const match of source.matchAll(/<input\b(?![^>]*\baria-label\s*=)[^>]*\bid\s*=\s*["']?([a-zA-Z0-9_-]+)["']?[^>]*>/gi)) {
|
|
1699
|
+
const id = match[1] ?? "";
|
|
1700
|
+
const labelRegex = new RegExp(`<label\\b[^>]*\\bfor\\s*=\\s*["']?${escapeRegExp(id)}["']?[^>]*>`, "i");
|
|
1701
|
+
if (!labelRegex.test(source)) {
|
|
1702
|
+
violations.push(createAccessibilityViolation("label", "critical", "Form fields should have labels.", match[0]));
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
const focusOrder = [...source.matchAll(/<(a|button|input|select|textarea)\b[^>]*>/gi)]
|
|
1706
|
+
.map((match) => match[0]);
|
|
1707
|
+
const visibleFocusFailures = focusOrder.filter((element) => /outline\s*:\s*(none|0)\b/i.test(element)).length;
|
|
1708
|
+
const missingSkipLinkCount = /href\s*=\s*["']#(main|content)["']/i.test(source) ? 0 : 1;
|
|
1709
|
+
if (visibleFocusFailures > 0) {
|
|
1710
|
+
violations.push(createAccessibilityViolation("focus-visible", "serious", "Keyboard focus indicators should remain visible."));
|
|
1711
|
+
}
|
|
1712
|
+
if (missingSkipLinkCount > 0) {
|
|
1713
|
+
violations.push(createAccessibilityViolation("skip-link", "moderate", "Pages should provide a skip link to the main content."));
|
|
1714
|
+
}
|
|
1715
|
+
return attachAccessibilityCounts({
|
|
1716
|
+
url: options.url,
|
|
1717
|
+
violations,
|
|
1718
|
+
keyboardFocus: {
|
|
1719
|
+
tabbableElementCount: focusOrder.length,
|
|
1720
|
+
visibleFocusFailures,
|
|
1721
|
+
missingSkipLinkCount,
|
|
1722
|
+
focusOrder
|
|
1723
|
+
}
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
static ScanHtml(options, html) {
|
|
1727
|
+
return LoadStrikeAccessibilityScanner.scanHtml(options, html);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
export class LoadStrikeAccessibilityAdapters {
|
|
1731
|
+
static async fromPlaywrightPageAsync(options, page) {
|
|
1732
|
+
const html = await readBrowserContent(page, ["ContentAsync", "contentAsync", "content"]);
|
|
1733
|
+
return LoadStrikeAccessibilityScanner.scanHtml(options, html);
|
|
1734
|
+
}
|
|
1735
|
+
static FromPlaywrightPageAsync(options, page) {
|
|
1736
|
+
return LoadStrikeAccessibilityAdapters.fromPlaywrightPageAsync(options, page);
|
|
1737
|
+
}
|
|
1738
|
+
static fromSeleniumDriver(options, driver) {
|
|
1739
|
+
const record = qualityRecord(driver);
|
|
1740
|
+
const html = String(qualityValue(record, "PageSource", "pageSource") ?? "");
|
|
1741
|
+
return LoadStrikeAccessibilityScanner.scanHtml(options, html);
|
|
1742
|
+
}
|
|
1743
|
+
static FromSeleniumDriver(options, driver) {
|
|
1744
|
+
return LoadStrikeAccessibilityAdapters.fromSeleniumDriver(options, driver);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
export class LoadStrikeBrowserWebVitalsCollector {
|
|
1748
|
+
static fromPerformanceSnapshot(options, snapshot) {
|
|
1749
|
+
validateAbsoluteUrl(options?.url, "Browser Web Vitals URL");
|
|
1750
|
+
return normalizeWebVitalsSnapshot(options.url, snapshot);
|
|
1751
|
+
}
|
|
1752
|
+
static FromPerformanceSnapshot(options, snapshot) {
|
|
1753
|
+
return LoadStrikeBrowserWebVitalsCollector.fromPerformanceSnapshot(options, snapshot);
|
|
1754
|
+
}
|
|
1755
|
+
static async fromPlaywrightPageAsync(options, page) {
|
|
1756
|
+
validateAbsoluteUrl(options?.url, "Browser Web Vitals URL");
|
|
1757
|
+
const snapshot = await invokeBrowserMethod(page, ["EvaluateAsync", "evaluate"], WEB_VITALS_SCRIPT);
|
|
1758
|
+
return normalizeWebVitalsSnapshot(options.url, snapshot);
|
|
1759
|
+
}
|
|
1760
|
+
static FromPlaywrightPageAsync(options, page) {
|
|
1761
|
+
return LoadStrikeBrowserWebVitalsCollector.fromPlaywrightPageAsync(options, page);
|
|
1762
|
+
}
|
|
1763
|
+
static fromSeleniumDriver(options, driver) {
|
|
1764
|
+
validateAbsoluteUrl(options?.url, "Browser Web Vitals URL");
|
|
1765
|
+
const snapshot = invokeBrowserMethodSync(driver, ["ExecuteScript", "executeScript"], WEB_VITALS_SCRIPT);
|
|
1766
|
+
return normalizeWebVitalsSnapshot(options.url, snapshot);
|
|
1767
|
+
}
|
|
1768
|
+
static FromSeleniumDriver(options, driver) {
|
|
1769
|
+
return LoadStrikeBrowserWebVitalsCollector.fromSeleniumDriver(options, driver);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1609
1772
|
export class LoadStrikeAccessibility {
|
|
1610
1773
|
static createScenario(name, options, check) {
|
|
1611
1774
|
if (typeof name !== "string" || !name.trim()) {
|
|
@@ -1654,6 +1817,129 @@ export class LoadStrikeBrowserWebVitals {
|
|
|
1654
1817
|
return LoadStrikeBrowserWebVitals.createScenario(name, options, measure);
|
|
1655
1818
|
}
|
|
1656
1819
|
}
|
|
1820
|
+
const WEB_VITALS_SCRIPT = `
|
|
1821
|
+
(() => {
|
|
1822
|
+
const navigation = performance.getEntriesByType("navigation")[0];
|
|
1823
|
+
const paint = performance.getEntriesByType("paint");
|
|
1824
|
+
const fcp = paint.find((entry) => entry.name === "first-contentful-paint");
|
|
1825
|
+
return {
|
|
1826
|
+
largestContentfulPaintMs: globalThis.__loadstrikeLcp ?? 0,
|
|
1827
|
+
interactionToNextPaintMs: globalThis.__loadstrikeInp ?? 0,
|
|
1828
|
+
cumulativeLayoutShift: globalThis.__loadstrikeCls ?? 0,
|
|
1829
|
+
firstContentfulPaintMs: fcp ? fcp.startTime : 0,
|
|
1830
|
+
timeToFirstByteMs: navigation ? navigation.responseStart : 0
|
|
1831
|
+
};
|
|
1832
|
+
})()
|
|
1833
|
+
`;
|
|
1834
|
+
function createAccessibilityViolation(ruleId, impact, description, target) {
|
|
1835
|
+
return {
|
|
1836
|
+
ruleId,
|
|
1837
|
+
impact,
|
|
1838
|
+
description,
|
|
1839
|
+
helpUrl: `https://www.w3.org/WAI/WCAG22/quickref/?versions=2.2#${ruleId}`,
|
|
1840
|
+
targets: target ? [target] : []
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
function attachAccessibilityCounts(result) {
|
|
1844
|
+
const violations = Array.isArray(result?.violations) ? result.violations : [];
|
|
1845
|
+
return {
|
|
1846
|
+
...result,
|
|
1847
|
+
violations,
|
|
1848
|
+
totalViolations: violations.length,
|
|
1849
|
+
criticalViolations: countAccessibilityImpact(violations, "critical"),
|
|
1850
|
+
seriousViolations: countAccessibilityImpact(violations, "serious"),
|
|
1851
|
+
moderateViolations: countAccessibilityImpact(violations, "moderate"),
|
|
1852
|
+
minorViolations: countAccessibilityImpact(violations, "minor")
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
function qualityRecord(value) {
|
|
1856
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
1857
|
+
? value
|
|
1858
|
+
: {};
|
|
1859
|
+
}
|
|
1860
|
+
function qualityValue(record, ...keys) {
|
|
1861
|
+
for (const key of keys) {
|
|
1862
|
+
if (key in record) {
|
|
1863
|
+
return record[key];
|
|
1864
|
+
}
|
|
1865
|
+
const normalized = key.toLowerCase();
|
|
1866
|
+
const match = Object.entries(record).find(([entryKey]) => entryKey.toLowerCase() === normalized);
|
|
1867
|
+
if (match) {
|
|
1868
|
+
return match[1];
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
return undefined;
|
|
1872
|
+
}
|
|
1873
|
+
function qualityStringArray(value) {
|
|
1874
|
+
if (!Array.isArray(value)) {
|
|
1875
|
+
return [];
|
|
1876
|
+
}
|
|
1877
|
+
return value.map((entry) => String(entry ?? "").trim()).filter(Boolean);
|
|
1878
|
+
}
|
|
1879
|
+
function qualityNumber(record, fallback, ...keys) {
|
|
1880
|
+
const value = qualityValue(record, ...keys);
|
|
1881
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1882
|
+
return value;
|
|
1883
|
+
}
|
|
1884
|
+
if (typeof value === "string" && value.trim()) {
|
|
1885
|
+
const parsed = Number(value);
|
|
1886
|
+
if (Number.isFinite(parsed)) {
|
|
1887
|
+
return parsed;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
return fallback;
|
|
1891
|
+
}
|
|
1892
|
+
function qualityOptionalNumber(record, ...keys) {
|
|
1893
|
+
const value = qualityValue(record, ...keys);
|
|
1894
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1895
|
+
return value;
|
|
1896
|
+
}
|
|
1897
|
+
if (typeof value === "string" && value.trim()) {
|
|
1898
|
+
const parsed = Number(value);
|
|
1899
|
+
if (Number.isFinite(parsed)) {
|
|
1900
|
+
return parsed;
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
return undefined;
|
|
1904
|
+
}
|
|
1905
|
+
function escapeRegExp(value) {
|
|
1906
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1907
|
+
}
|
|
1908
|
+
async function readBrowserContent(target, methodNames) {
|
|
1909
|
+
const value = await invokeBrowserMethod(target, methodNames);
|
|
1910
|
+
return String(value ?? "");
|
|
1911
|
+
}
|
|
1912
|
+
async function invokeBrowserMethod(target, methodNames, ...args) {
|
|
1913
|
+
const record = qualityRecord(target);
|
|
1914
|
+
for (const name of methodNames) {
|
|
1915
|
+
const method = record[name];
|
|
1916
|
+
if (typeof method === "function") {
|
|
1917
|
+
return await method.apply(target, args);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
throw new Error(`Browser object does not expose ${methodNames.join(" or ")}.`);
|
|
1921
|
+
}
|
|
1922
|
+
function invokeBrowserMethodSync(target, methodNames, ...args) {
|
|
1923
|
+
const record = qualityRecord(target);
|
|
1924
|
+
for (const name of methodNames) {
|
|
1925
|
+
const method = record[name];
|
|
1926
|
+
if (typeof method === "function") {
|
|
1927
|
+
return method.apply(target, args);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
throw new Error(`Browser object does not expose ${methodNames.join(" or ")}.`);
|
|
1931
|
+
}
|
|
1932
|
+
function normalizeWebVitalsSnapshot(url, snapshot) {
|
|
1933
|
+
const source = qualityRecord(snapshot);
|
|
1934
|
+
return {
|
|
1935
|
+
url,
|
|
1936
|
+
largestContentfulPaintMs: qualityNumber(source, 0, "largestContentfulPaintMs", "LargestContentfulPaintMs", "LCP", "lcp"),
|
|
1937
|
+
interactionToNextPaintMs: qualityNumber(source, 0, "interactionToNextPaintMs", "InteractionToNextPaintMs", "INP", "inp"),
|
|
1938
|
+
cumulativeLayoutShift: qualityNumber(source, 0, "cumulativeLayoutShift", "CumulativeLayoutShift", "CLS", "cls"),
|
|
1939
|
+
firstContentfulPaintMs: qualityNumber(source, 0, "firstContentfulPaintMs", "FirstContentfulPaintMs", "FCP", "fcp"),
|
|
1940
|
+
timeToFirstByteMs: qualityNumber(source, 0, "timeToFirstByteMs", "TimeToFirstByteMs", "TTFB", "ttfb")
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1657
1943
|
function validateAbsoluteUrl(value, label) {
|
|
1658
1944
|
if (typeof value !== "string" || !value.trim()) {
|
|
1659
1945
|
throw new Error(`${label} must be provided.`);
|
|
@@ -1944,6 +2230,26 @@ export class LoadStrikeScenario {
|
|
|
1944
2230
|
__loadStrikeInternalLicenseFeatures() {
|
|
1945
2231
|
return [...this.internalLicenseFeatures];
|
|
1946
2232
|
}
|
|
2233
|
+
__loadStrikeScenarioSourceAnalysis() {
|
|
2234
|
+
const source = this.runHandler.toString();
|
|
2235
|
+
const lines = source
|
|
2236
|
+
.split(/\r?\n/)
|
|
2237
|
+
.map((line) => line.trim())
|
|
2238
|
+
.filter((line) => line.length > 0 && !line.startsWith("//"));
|
|
2239
|
+
const lineCount = Math.max(lines.length, 1);
|
|
2240
|
+
return {
|
|
2241
|
+
ScenarioName: this.name,
|
|
2242
|
+
Language: "TypeScript/JavaScript",
|
|
2243
|
+
AnalyzerVersion: "function-source-v1",
|
|
2244
|
+
ScenarioBlockLineCount: lineCount,
|
|
2245
|
+
LocalMethodLineCount: 0,
|
|
2246
|
+
TotalCountedLineCount: lineCount,
|
|
2247
|
+
IgnoredExternalCallCount: 0,
|
|
2248
|
+
Warnings: [
|
|
2249
|
+
"Function-source analysis counts the scenario handler source available at runtime; imported library implementation lines are ignored."
|
|
2250
|
+
]
|
|
2251
|
+
};
|
|
2252
|
+
}
|
|
1947
2253
|
__loadStrikeWithInternalLicenseFeatures(...features) {
|
|
1948
2254
|
const merged = Array.from(new Set([...this.internalLicenseFeatures, ...normalizeStringArray(features)]));
|
|
1949
2255
|
return new LoadStrikeScenario(this.name, this.runHandler, this.initHandler, this.cleanHandler, this.loadSimulations, this.thresholds, this.trackingConfiguration, this.maxFailCount, this.withoutWarmUpValue, this.warmUpDurationSeconds, this.weight, this.restartIterationOnFail, merged);
|
|
@@ -8037,7 +8343,8 @@ function buildLicenseValidationPayload(options, scenarios) {
|
|
|
8037
8343
|
Weight: scenario.getWeight(),
|
|
8038
8344
|
LoadSimulations: [...scenario.getSimulations()],
|
|
8039
8345
|
Thresholds: [...scenario.getThresholds()],
|
|
8040
|
-
Tracking: scenario.getTrackingConfiguration() ?? {}
|
|
8346
|
+
Tracking: scenario.getTrackingConfiguration() ?? {},
|
|
8347
|
+
ScenarioSourceAnalysis: scenario.__loadStrikeScenarioSourceAnalysis()
|
|
8041
8348
|
};
|
|
8042
8349
|
const internalLicenseFeatures = scenario.__loadStrikeInternalLicenseFeatures();
|
|
8043
8350
|
if (internalLicenseFeatures.length > 0) {
|
package/dist/esm/sinks.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { LoadStrikePluginData as LoadStrikePluginDataModel, LoadStrikePluginDataTable as LoadStrikePluginDataTableModel } from "./runtime.js";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
import { createSocket } from "node:dgram";
|
|
3
6
|
import { Pool } from "pg";
|
|
4
7
|
const DEFAULT_INFLUX_CONFIGURATION_SECTION_PATH = "LoadStrike:ReportingSinks:InfluxDb";
|
|
5
8
|
const DEFAULT_GRAFANA_LOKI_CONFIGURATION_SECTION_PATH = "LoadStrike:ReportingSinks:GrafanaLoki";
|
|
@@ -1364,6 +1367,226 @@ export class OtelCollectorReportingSink {
|
|
|
1364
1367
|
}, resolveTimeoutMs(this.options.timeoutSeconds, this.options.timeoutMs), "OtelCollectorReportingSink metrics");
|
|
1365
1368
|
}
|
|
1366
1369
|
}
|
|
1370
|
+
class ExpandedEventReportingSink {
|
|
1371
|
+
constructor(sinkName, licenseFeature) {
|
|
1372
|
+
this.baseContext = null;
|
|
1373
|
+
this.session = null;
|
|
1374
|
+
this.sinkName = sinkName;
|
|
1375
|
+
this.SinkName = sinkName;
|
|
1376
|
+
this.licenseFeature = licenseFeature;
|
|
1377
|
+
this.LicenseFeature = licenseFeature;
|
|
1378
|
+
}
|
|
1379
|
+
init(context, _infraConfig) {
|
|
1380
|
+
this.baseContext = cloneBaseContext(context);
|
|
1381
|
+
}
|
|
1382
|
+
Init(context, infraConfig) {
|
|
1383
|
+
this.init(context, infraConfig);
|
|
1384
|
+
}
|
|
1385
|
+
start(session) {
|
|
1386
|
+
this.session = sinkSessionMetadataFromContext(this.getBaseContext(), session);
|
|
1387
|
+
}
|
|
1388
|
+
Start(session) {
|
|
1389
|
+
this.start(session);
|
|
1390
|
+
}
|
|
1391
|
+
async saveRealtimeStats(scenarioStats) {
|
|
1392
|
+
await this.persistEvents(createRealtimeStatsEvents(this.getSession(), scenarioStats));
|
|
1393
|
+
}
|
|
1394
|
+
async SaveRealtimeStats(scenarioStats) {
|
|
1395
|
+
await this.saveRealtimeStats(scenarioStats);
|
|
1396
|
+
}
|
|
1397
|
+
async saveRealtimeMetrics(metrics) {
|
|
1398
|
+
await this.persistEvents(createRealtimeMetricEvents(this.getSession(), metrics));
|
|
1399
|
+
}
|
|
1400
|
+
async SaveRealtimeMetrics(metrics) {
|
|
1401
|
+
await this.saveRealtimeMetrics(metrics);
|
|
1402
|
+
}
|
|
1403
|
+
async saveRunResult(result) {
|
|
1404
|
+
await this.persistEvents(createRunResultEvents(this.getSession(), result));
|
|
1405
|
+
}
|
|
1406
|
+
async SaveRunResult(result) {
|
|
1407
|
+
await this.saveRunResult(result);
|
|
1408
|
+
}
|
|
1409
|
+
stop() {
|
|
1410
|
+
this.session = null;
|
|
1411
|
+
}
|
|
1412
|
+
Stop() {
|
|
1413
|
+
this.stop();
|
|
1414
|
+
}
|
|
1415
|
+
Dispose() {
|
|
1416
|
+
this.baseContext = null;
|
|
1417
|
+
this.stop();
|
|
1418
|
+
}
|
|
1419
|
+
getBaseContext() {
|
|
1420
|
+
if (!this.baseContext) {
|
|
1421
|
+
throw new Error(`${this.sinkName} has not been initialized.`);
|
|
1422
|
+
}
|
|
1423
|
+
return this.baseContext;
|
|
1424
|
+
}
|
|
1425
|
+
getSession() {
|
|
1426
|
+
if (!this.session) {
|
|
1427
|
+
throw new Error(`${this.sinkName} has not been started.`);
|
|
1428
|
+
}
|
|
1429
|
+
return this.session;
|
|
1430
|
+
}
|
|
1431
|
+
buildPayload(events, staticTags = {}) {
|
|
1432
|
+
return {
|
|
1433
|
+
sinkName: this.sinkName,
|
|
1434
|
+
runId: this.getSession().runId,
|
|
1435
|
+
sessionId: this.getSession().sessionId,
|
|
1436
|
+
events: events.map((event) => ({
|
|
1437
|
+
...event,
|
|
1438
|
+
tags: { ...staticTags, ...event.tags }
|
|
1439
|
+
})),
|
|
1440
|
+
metrics: createReportingSinkMetricPoints(this.sinkName, events, staticTags)
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
class ExpandedHttpJsonReportingSink extends ExpandedEventReportingSink {
|
|
1445
|
+
constructor(sinkName, licenseFeature, defaultEndpointPath, options = {}) {
|
|
1446
|
+
super(sinkName, licenseFeature);
|
|
1447
|
+
const source = asRecord(options);
|
|
1448
|
+
this.baseUrl = optionString(source, "baseUrl", "BaseUrl").trim();
|
|
1449
|
+
this.endpointPath = optionString(source, "endpointPath", "EndpointPath").trim() || defaultEndpointPath;
|
|
1450
|
+
this.headers = normalizeStringMap(optionRecord(source, "headers", "Headers"));
|
|
1451
|
+
this.staticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
|
|
1452
|
+
this.timeoutMs = resolveTimeoutMs(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"), optionNumber(source, "timeoutMs", "TimeoutMs"));
|
|
1453
|
+
this.fetchImpl = pickRecordValue(source, "fetchImpl", "FetchImpl") ?? fetch;
|
|
1454
|
+
}
|
|
1455
|
+
init(context, infraConfig) {
|
|
1456
|
+
super.init(context, infraConfig);
|
|
1457
|
+
if (!this.baseUrl.trim()) {
|
|
1458
|
+
throw new Error(`${this.constructor.name} requires BaseUrl.`);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
async persistEvents(events) {
|
|
1462
|
+
if (!events.length) {
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
await postWithTimeout(this.fetchImpl, `${trimTrailingSlashes(this.baseUrl)}${normalizePath(this.endpointPath)}`, {
|
|
1466
|
+
method: "POST",
|
|
1467
|
+
headers: {
|
|
1468
|
+
"Content-Type": "application/json",
|
|
1469
|
+
...this.headers
|
|
1470
|
+
},
|
|
1471
|
+
body: JSON.stringify(this.buildPayload(events, this.staticTags))
|
|
1472
|
+
}, this.timeoutMs, this.constructor.name);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
export class PrometheusRemoteWriteReportingSink extends ExpandedHttpJsonReportingSink {
|
|
1476
|
+
constructor(options = {}) {
|
|
1477
|
+
super("prometheus-remote-write", "extensions.reporting_sinks.prometheus_remote_write", "/api/v1/write", options);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
export class CloudWatchReportingSink extends ExpandedHttpJsonReportingSink {
|
|
1481
|
+
constructor(options = {}) {
|
|
1482
|
+
super("cloudwatch", "extensions.reporting_sinks.cloudwatch", "/loadstrike/events", options);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
export class DynatraceReportingSink extends ExpandedHttpJsonReportingSink {
|
|
1486
|
+
constructor(options = {}) {
|
|
1487
|
+
super("dynatrace", "extensions.reporting_sinks.dynatrace", "/api/v2/logs/ingest", options);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
export class ElasticsearchReportingSink extends ExpandedHttpJsonReportingSink {
|
|
1491
|
+
constructor(options = {}) {
|
|
1492
|
+
super("elasticsearch", "extensions.reporting_sinks.elasticsearch", "/loadstrike-events/_doc", options);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
export class OpenSearchReportingSink extends ExpandedHttpJsonReportingSink {
|
|
1496
|
+
constructor(options = {}) {
|
|
1497
|
+
super("opensearch", "extensions.reporting_sinks.opensearch", "/loadstrike-events/_doc", options);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
export class NewRelicReportingSink extends ExpandedHttpJsonReportingSink {
|
|
1501
|
+
constructor(options = {}) {
|
|
1502
|
+
super("new-relic", "extensions.reporting_sinks.new_relic", "/log/v1", options);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
export class GenericWebhookReportingSink extends ExpandedHttpJsonReportingSink {
|
|
1506
|
+
constructor(options = {}) {
|
|
1507
|
+
super("webhook", "extensions.reporting_sinks.webhook", "/loadstrike", options);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
export class KafkaReportingSink extends ExpandedEventReportingSink {
|
|
1511
|
+
constructor(options = {}) {
|
|
1512
|
+
super("kafka", "extensions.reporting_sinks.kafka");
|
|
1513
|
+
const source = asRecord(options);
|
|
1514
|
+
this.topic = optionString(source, "topic", "Topic").trim() || "loadstrike-events";
|
|
1515
|
+
this.staticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
|
|
1516
|
+
this.publishAsync = pickRecordValue(source, "publishAsync", "PublishAsync");
|
|
1517
|
+
}
|
|
1518
|
+
async persistEvents(events) {
|
|
1519
|
+
if (!events.length) {
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
if (!this.publishAsync) {
|
|
1523
|
+
throw new Error("KafkaReportingSink requires PublishAsync when a Kafka producer is not configured by the host application.");
|
|
1524
|
+
}
|
|
1525
|
+
await this.publishAsync(this.topic, JSON.stringify(this.buildPayload(events, this.staticTags)));
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
class StatsDReportingSinkBase extends ExpandedEventReportingSink {
|
|
1529
|
+
constructor(sinkName, licenseFeature, options = {}, dogStatsD = false) {
|
|
1530
|
+
super(sinkName, licenseFeature);
|
|
1531
|
+
const source = asRecord(options);
|
|
1532
|
+
this.prefix = optionString(source, "prefix", "Prefix").trim() || "loadstrike";
|
|
1533
|
+
this.host = optionString(source, "host", "Host").trim() || "127.0.0.1";
|
|
1534
|
+
this.port = optionNumber(source, "port", "Port") ?? 8125;
|
|
1535
|
+
this.tags = normalizeStringMap(optionRecord(source, "tags", "Tags"));
|
|
1536
|
+
this.sendLineAsync = pickRecordValue(source, "sendLineAsync", "SendLineAsync");
|
|
1537
|
+
this.dogStatsD = dogStatsD;
|
|
1538
|
+
}
|
|
1539
|
+
async persistEvents(events) {
|
|
1540
|
+
const points = createReportingSinkMetricPoints(this.sinkName, events, this.tags);
|
|
1541
|
+
for (const point of points) {
|
|
1542
|
+
await this.sendLine(this.formatPoint(point));
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
formatPoint(point) {
|
|
1546
|
+
const metricType = point.metricKind === "count" ? "c" : "g";
|
|
1547
|
+
const tagSuffix = this.dogStatsD
|
|
1548
|
+
? dogStatsDTagSuffix({ ...this.tags, ...point.tags })
|
|
1549
|
+
: "";
|
|
1550
|
+
return `${this.prefix}.${sanitizeMetricToken(point.metricName)}:${point.value}|${metricType}${tagSuffix}`;
|
|
1551
|
+
}
|
|
1552
|
+
async sendLine(line) {
|
|
1553
|
+
if (this.sendLineAsync) {
|
|
1554
|
+
await this.sendLineAsync(line);
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
await sendStatsDUdpLine(this.host, this.port, line);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
export class StatsDReportingSink extends StatsDReportingSinkBase {
|
|
1561
|
+
constructor(options = {}) {
|
|
1562
|
+
super("statsd", "extensions.reporting_sinks.statsd", options, false);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
export class DogStatsDReportingSink extends StatsDReportingSinkBase {
|
|
1566
|
+
constructor(options = {}) {
|
|
1567
|
+
super("dogstatsd", "extensions.reporting_sinks.dogstatsd", options, true);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
export class NetdataStatsDReportingSink extends StatsDReportingSinkBase {
|
|
1571
|
+
constructor(options = {}) {
|
|
1572
|
+
super("netdata-statsd", "extensions.reporting_sinks.netdata", options, false);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
export class JsonlFileReportingSink extends ExpandedEventReportingSink {
|
|
1576
|
+
constructor(options = {}) {
|
|
1577
|
+
super("jsonl", "extensions.reporting_sinks.jsonl");
|
|
1578
|
+
const source = asRecord(options);
|
|
1579
|
+
this.filePath = optionString(source, "filePath", "FilePath").trim() || "loadstrike-reporting.jsonl";
|
|
1580
|
+
this.staticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
|
|
1581
|
+
}
|
|
1582
|
+
async persistEvents(events) {
|
|
1583
|
+
if (!events.length) {
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
1587
|
+
appendFileSync(this.filePath, `${JSON.stringify(this.buildPayload(events, this.staticTags))}\n`, "utf8");
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1367
1590
|
function createRealtimeStatsEvents(session, scenarios) {
|
|
1368
1591
|
const occurredUtc = new Date();
|
|
1369
1592
|
const events = [];
|
|
@@ -2577,6 +2800,32 @@ function normalizeStringMap(value) {
|
|
|
2577
2800
|
}
|
|
2578
2801
|
return rows;
|
|
2579
2802
|
}
|
|
2803
|
+
function sanitizeMetricToken(value) {
|
|
2804
|
+
return String(value ?? "")
|
|
2805
|
+
.trim()
|
|
2806
|
+
.replace(/[^A-Za-z0-9_.-]+/g, "_")
|
|
2807
|
+
.replace(/^_+|_+$/g, "") || "metric";
|
|
2808
|
+
}
|
|
2809
|
+
function dogStatsDTagSuffix(tags) {
|
|
2810
|
+
const values = Object.entries(tags)
|
|
2811
|
+
.filter(([key]) => key.trim().length > 0)
|
|
2812
|
+
.map(([key, value]) => `${sanitizeMetricToken(key)}:${String(value ?? "").replace(/[,|]+/g, "_")}`);
|
|
2813
|
+
return values.length ? `|#${values.join(",")}` : "";
|
|
2814
|
+
}
|
|
2815
|
+
function sendStatsDUdpLine(host, port, line) {
|
|
2816
|
+
return new Promise((resolve, reject) => {
|
|
2817
|
+
const socket = createSocket("udp4");
|
|
2818
|
+
const payload = Buffer.from(line, "utf8");
|
|
2819
|
+
socket.send(payload, port, host, (error) => {
|
|
2820
|
+
socket.close();
|
|
2821
|
+
if (error) {
|
|
2822
|
+
reject(error);
|
|
2823
|
+
return;
|
|
2824
|
+
}
|
|
2825
|
+
resolve();
|
|
2826
|
+
});
|
|
2827
|
+
});
|
|
2828
|
+
}
|
|
2580
2829
|
function resolveTimeoutMs(timeoutSeconds, timeoutMs) {
|
|
2581
2830
|
if (Number.isFinite(timeoutMs) && Number(timeoutMs) > 0) {
|
|
2582
2831
|
return Math.max(Math.trunc(Number(timeoutMs)), 1);
|