@loadstrike/loadstrike-sdk 1.0.27101 → 1.0.27301

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.
@@ -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);