@loadstrike/loadstrike-sdk 1.0.30201 → 1.0.31001

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/dist/cjs/sinks.js CHANGED
@@ -4,10 +4,44 @@ exports.__loadstrikeTestExports = exports.JsonlFileReportingSink = exports.Netda
4
4
  exports.cloneReportingSinkForRun = cloneReportingSinkForRun;
5
5
  const runtime_js_1 = require("./runtime.js");
6
6
  const node_crypto_1 = require("node:crypto");
7
- const node_fs_1 = require("node:fs");
7
+ const promises_1 = require("node:fs/promises");
8
8
  const node_path_1 = require("node:path");
9
9
  const node_dgram_1 = require("node:dgram");
10
+ const node_zlib_1 = require("node:zlib");
10
11
  const pg_1 = require("pg");
12
+ const iteration_observations_js_1 = require("./iteration-observations.js");
13
+ const iteration_observation_diagnostics_js_1 = require("./iteration-observation-diagnostics.js");
14
+ const PORTAL_RUN_TOKEN_PROVIDER = Symbol.for("loadstrike.internal.portal-run-token-provider");
15
+ const MAXIMUM_HTTP_RESPONSE_BODY_BYTES = 256 * 1024;
16
+ const MAXIMUM_HTTP_METADATA_CHARACTERS = 256;
17
+ const HTTP_METADATA_TRUNCATION_SUFFIX = " [truncated]";
18
+ class ReportingSinkHttpError extends Error {
19
+ constructor(sinkName, status, statusText, requestId) {
20
+ const normalizedStatus = Number.isFinite(status) ? Math.max(Math.trunc(status), 0) : 0;
21
+ const normalizedStatusText = normalizeHttpMetadata(statusText);
22
+ const normalizedRequestId = normalizeHttpMetadata(requestId);
23
+ super(`${sinkName} write failed with HTTP status ${normalizedStatus}`
24
+ + (normalizedStatusText ? ` ${normalizedStatusText}` : "")
25
+ + (normalizedRequestId ? ` (requestId=${normalizedRequestId})` : "")
26
+ + ".");
27
+ this.name = "ReportingSinkHttpError";
28
+ this.status = normalizedStatus;
29
+ this.statusText = normalizedStatusText;
30
+ this.requestId = normalizedRequestId;
31
+ }
32
+ }
33
+ function gzipJsonAsync(value) {
34
+ const json = Buffer.from(JSON.stringify(value), "utf8");
35
+ return new Promise((resolve, reject) => {
36
+ (0, node_zlib_1.gzip)(json, (error, compressed) => {
37
+ if (error) {
38
+ reject(error);
39
+ return;
40
+ }
41
+ resolve(compressed);
42
+ });
43
+ });
44
+ }
11
45
  const DEFAULT_INFLUX_CONFIGURATION_SECTION_PATH = "LoadStrike:ReportingSinks:InfluxDb";
12
46
  const DEFAULT_GRAFANA_LOKI_CONFIGURATION_SECTION_PATH = "LoadStrike:ReportingSinks:GrafanaLoki";
13
47
  const DEFAULT_TIMESCALEDB_CONFIGURATION_SECTION_PATH = "LoadStrike:ReportingSinks:TimescaleDb";
@@ -376,6 +410,28 @@ class CompositeReportingSink {
376
410
  async SaveRunResult(result) {
377
411
  await this.saveRunResult(result);
378
412
  }
413
+ async saveIterationBatch(batch) {
414
+ for (const sink of this.sinks) {
415
+ const saveIterationBatch = sink.saveIterationBatch ?? sink.SaveIterationBatch;
416
+ if (saveIterationBatch) {
417
+ await saveIterationBatch.call(sink, batch);
418
+ }
419
+ }
420
+ }
421
+ async SaveIterationBatch(batch) {
422
+ await this.saveIterationBatch(batch);
423
+ }
424
+ async completeIterationObservationStream(completion) {
425
+ for (const sink of this.sinks) {
426
+ const completeIterationObservationStream = sink.completeIterationObservationStream ?? sink.CompleteIterationObservationStream;
427
+ if (completeIterationObservationStream) {
428
+ await completeIterationObservationStream.call(sink, completion);
429
+ }
430
+ }
431
+ }
432
+ async CompleteIterationObservationStream(completion) {
433
+ await this.completeIterationObservationStream(completion);
434
+ }
379
435
  async stop() {
380
436
  for (const sink of this.sinks) {
381
437
  const stop = sink.stop ?? sink.Stop;
@@ -391,6 +447,7 @@ class CompositeReportingSink {
391
447
  exports.CompositeReportingSink = CompositeReportingSink;
392
448
  class PortalReportingSink {
393
449
  constructor(options = {}) {
450
+ this.iterationObservationPortalSink = true;
394
451
  this.sinkName = "portal";
395
452
  this.SinkName = "portal";
396
453
  this.licenseFeature = "extensions.reporting_sinks.portal";
@@ -398,6 +455,7 @@ class PortalReportingSink {
398
455
  this.baseContext = null;
399
456
  this.session = null;
400
457
  this.runToken = "";
458
+ this.runTokenProvider = null;
401
459
  this.ingestUrl = "";
402
460
  this.optionsInput = { ...options };
403
461
  const source = asRecord(options);
@@ -414,7 +472,11 @@ class PortalReportingSink {
414
472
  this.init(context, infraConfig);
415
473
  }
416
474
  start(session) {
417
- this.runToken = String(session.runToken ?? session.RunToken ?? "").trim();
475
+ const internalProvider = session[PORTAL_RUN_TOKEN_PROVIDER];
476
+ this.runTokenProvider = typeof internalProvider === "function"
477
+ ? internalProvider
478
+ : () => String(session.runToken ?? session.RunToken ?? "").trim();
479
+ this.runToken = this.runTokenProvider();
418
480
  this.ingestUrl = String(session.portalReportingIngestUrl ?? session.PortalReportingIngestUrl ?? "").trim();
419
481
  if (!this.runToken || !this.ingestUrl) {
420
482
  throw new Error("PortalReportingSink requires a managed portal reporting session.");
@@ -444,8 +506,27 @@ class PortalReportingSink {
444
506
  async SaveRunResult(result) {
445
507
  await this.saveRunResult(result);
446
508
  }
509
+ async saveIterationBatch(batch) {
510
+ const compressed = await gzipJsonAsync(batch);
511
+ await this.persistObservationPayload({
512
+ compressedObservationBatches: [{
513
+ compression: "gzip-json",
514
+ payloadBase64: compressed.toString("base64")
515
+ }]
516
+ });
517
+ }
518
+ async SaveIterationBatch(batch) {
519
+ await this.saveIterationBatch(batch);
520
+ }
521
+ async completeIterationObservationStream(completion) {
522
+ await this.persistObservationPayload({ observationStreamCompletions: [completion] });
523
+ }
524
+ async CompleteIterationObservationStream(completion) {
525
+ await this.completeIterationObservationStream(completion);
526
+ }
447
527
  stop() {
448
528
  this.session = null;
529
+ this.runTokenProvider = null;
449
530
  }
450
531
  Stop() {
451
532
  this.stop();
@@ -474,12 +555,27 @@ class PortalReportingSink {
474
555
  method: "POST",
475
556
  headers: { "Content-Type": "application/json" },
476
557
  body: JSON.stringify({
477
- runToken: this.runToken,
478
- events: events.map((event, index) => portalEventPayload(event, index))
558
+ runToken: this.currentRunToken(),
559
+ events: events.map((event, index) => portalEventPayload(removeSdkPercentileFields(event), index))
560
+ })
561
+ }, this.timeoutMs, "PortalReportingSink");
562
+ validatePortalIngestResponse(responseBody);
563
+ }
564
+ async persistObservationPayload(payload) {
565
+ const responseBody = await postWithTimeout(this.fetchImpl, this.ingestUrl, {
566
+ method: "POST",
567
+ headers: { "Content-Type": "application/json" },
568
+ body: JSON.stringify({
569
+ runToken: this.currentRunToken(),
570
+ ...payload
479
571
  })
480
572
  }, this.timeoutMs, "PortalReportingSink");
481
573
  validatePortalIngestResponse(responseBody);
482
574
  }
575
+ currentRunToken() {
576
+ const current = this.runTokenProvider?.() ?? "";
577
+ return current || this.runToken;
578
+ }
483
579
  }
484
580
  exports.PortalReportingSink = PortalReportingSink;
485
581
  function cloneReportingSinkForRun(sink) {
@@ -556,6 +652,18 @@ class InfluxDbReportingSink {
556
652
  async SaveRunResult(result) {
557
653
  await this.saveRunResult(result);
558
654
  }
655
+ async saveIterationBatch(batch) {
656
+ await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
657
+ }
658
+ async SaveIterationBatch(batch) {
659
+ await this.saveIterationBatch(batch);
660
+ }
661
+ async completeIterationObservationStream(completion) {
662
+ await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
663
+ }
664
+ async CompleteIterationObservationStream(completion) {
665
+ await this.completeIterationObservationStream(completion);
666
+ }
559
667
  stop() {
560
668
  this.session = null;
561
669
  }
@@ -666,6 +774,18 @@ class GrafanaLokiReportingSink {
666
774
  async SaveRunResult(result) {
667
775
  await this.saveRunResult(result);
668
776
  }
777
+ async saveIterationBatch(batch) {
778
+ await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
779
+ }
780
+ async SaveIterationBatch(batch) {
781
+ await this.saveIterationBatch(batch);
782
+ }
783
+ async completeIterationObservationStream(completion) {
784
+ await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
785
+ }
786
+ async CompleteIterationObservationStream(completion) {
787
+ await this.completeIterationObservationStream(completion);
788
+ }
669
789
  stop() {
670
790
  this.session = null;
671
791
  }
@@ -812,6 +932,18 @@ class TimescaleDbReportingSink {
812
932
  async SaveRunResult(result) {
813
933
  await this.saveRunResult(result);
814
934
  }
935
+ async saveIterationBatch(batch) {
936
+ await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
937
+ }
938
+ async SaveIterationBatch(batch) {
939
+ await this.saveIterationBatch(batch);
940
+ }
941
+ async completeIterationObservationStream(completion) {
942
+ await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
943
+ }
944
+ async CompleteIterationObservationStream(completion) {
945
+ await this.completeIterationObservationStream(completion);
946
+ }
815
947
  async stop() {
816
948
  this.session = null;
817
949
  if (this.pool) {
@@ -1088,6 +1220,18 @@ class DatadogReportingSink {
1088
1220
  async SaveRunResult(result) {
1089
1221
  await this.saveRunResult(result);
1090
1222
  }
1223
+ async saveIterationBatch(batch) {
1224
+ await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
1225
+ }
1226
+ async SaveIterationBatch(batch) {
1227
+ await this.saveIterationBatch(batch);
1228
+ }
1229
+ async completeIterationObservationStream(completion) {
1230
+ await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
1231
+ }
1232
+ async CompleteIterationObservationStream(completion) {
1233
+ await this.completeIterationObservationStream(completion);
1234
+ }
1091
1235
  stop() {
1092
1236
  this.session = null;
1093
1237
  }
@@ -1211,6 +1355,18 @@ class SplunkReportingSink {
1211
1355
  async SaveRunResult(result) {
1212
1356
  await this.saveRunResult(result);
1213
1357
  }
1358
+ async saveIterationBatch(batch) {
1359
+ await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
1360
+ }
1361
+ async SaveIterationBatch(batch) {
1362
+ await this.saveIterationBatch(batch);
1363
+ }
1364
+ async completeIterationObservationStream(completion) {
1365
+ await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
1366
+ }
1367
+ async CompleteIterationObservationStream(completion) {
1368
+ await this.completeIterationObservationStream(completion);
1369
+ }
1214
1370
  stop() {
1215
1371
  this.session = null;
1216
1372
  }
@@ -1314,6 +1470,18 @@ class OtelCollectorReportingSink {
1314
1470
  async SaveRunResult(result) {
1315
1471
  await this.saveRunResult(result);
1316
1472
  }
1473
+ async saveIterationBatch(batch) {
1474
+ await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
1475
+ }
1476
+ async SaveIterationBatch(batch) {
1477
+ await this.saveIterationBatch(batch);
1478
+ }
1479
+ async completeIterationObservationStream(completion) {
1480
+ await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
1481
+ }
1482
+ async CompleteIterationObservationStream(completion) {
1483
+ await this.completeIterationObservationStream(completion);
1484
+ }
1317
1485
  stop() {
1318
1486
  this.session = null;
1319
1487
  }
@@ -1391,6 +1559,7 @@ class ExpandedEventReportingSink {
1391
1559
  constructor(sinkName, licenseFeature) {
1392
1560
  this.baseContext = null;
1393
1561
  this.session = null;
1562
+ this.preservesRawIterationObservations = true;
1394
1563
  this.sinkName = sinkName;
1395
1564
  this.SinkName = sinkName;
1396
1565
  this.licenseFeature = licenseFeature;
@@ -1426,6 +1595,18 @@ class ExpandedEventReportingSink {
1426
1595
  async SaveRunResult(result) {
1427
1596
  await this.saveRunResult(result);
1428
1597
  }
1598
+ async saveIterationBatch(batch) {
1599
+ await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
1600
+ }
1601
+ async SaveIterationBatch(batch) {
1602
+ await this.saveIterationBatch(batch);
1603
+ }
1604
+ async completeIterationObservationStream(completion) {
1605
+ await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
1606
+ }
1607
+ async CompleteIterationObservationStream(completion) {
1608
+ await this.completeIterationObservationStream(completion);
1609
+ }
1429
1610
  stop() {
1430
1611
  this.session = null;
1431
1612
  }
@@ -1449,15 +1630,18 @@ class ExpandedEventReportingSink {
1449
1630
  return this.session;
1450
1631
  }
1451
1632
  buildPayload(events, staticTags = {}) {
1633
+ const projectedEvents = this.preservesRawIterationObservations
1634
+ ? events.map(removeSdkPercentileFields)
1635
+ : events;
1452
1636
  return {
1453
1637
  sinkName: this.sinkName,
1454
1638
  runId: this.getSession().runId,
1455
1639
  sessionId: this.getSession().sessionId,
1456
- events: events.map((event) => ({
1640
+ events: projectedEvents.map((event) => ({
1457
1641
  ...event,
1458
1642
  tags: { ...staticTags, ...event.tags }
1459
1643
  })),
1460
- metrics: createReportingSinkMetricPoints(this.sinkName, events, staticTags)
1644
+ metrics: createReportingSinkMetricPoints(this.sinkName, projectedEvents, staticTags)
1461
1645
  };
1462
1646
  }
1463
1647
  }
@@ -1491,22 +1675,43 @@ class ExpandedHttpJsonReportingSink extends ExpandedEventReportingSink {
1491
1675
  body: JSON.stringify(this.buildPayload(events, this.staticTags))
1492
1676
  }, this.timeoutMs, this.constructor.name);
1493
1677
  }
1678
+ async saveIterationBatch(batch) {
1679
+ await this.persistCanonicalGzipPayload(batch);
1680
+ }
1681
+ async completeIterationObservationStream(completion) {
1682
+ await this.persistCanonicalGzipPayload(completion);
1683
+ }
1684
+ async persistCanonicalGzipPayload(payload) {
1685
+ const compressed = await gzipJsonAsync(payload);
1686
+ await postWithTimeout(this.fetchImpl, `${trimTrailingSlashes(this.baseUrl)}${normalizePath(this.endpointPath)}`, {
1687
+ method: "POST",
1688
+ headers: {
1689
+ "Content-Type": "application/json",
1690
+ "Content-Encoding": "gzip",
1691
+ ...this.headers
1692
+ },
1693
+ body: Uint8Array.from(compressed).buffer
1694
+ }, this.timeoutMs, this.constructor.name);
1695
+ }
1494
1696
  }
1495
1697
  class PrometheusRemoteWriteReportingSink extends ExpandedHttpJsonReportingSink {
1496
1698
  constructor(options = {}) {
1497
1699
  super("prometheus-remote-write", "extensions.reporting_sinks.prometheus_remote_write", "/api/v1/write", options);
1700
+ this.iterationObservationShapeLimited = true;
1498
1701
  }
1499
1702
  }
1500
1703
  exports.PrometheusRemoteWriteReportingSink = PrometheusRemoteWriteReportingSink;
1501
1704
  class CloudWatchReportingSink extends ExpandedHttpJsonReportingSink {
1502
1705
  constructor(options = {}) {
1503
1706
  super("cloudwatch", "extensions.reporting_sinks.cloudwatch", "/loadstrike/events", options);
1707
+ this.iterationObservationShapeLimited = true;
1504
1708
  }
1505
1709
  }
1506
1710
  exports.CloudWatchReportingSink = CloudWatchReportingSink;
1507
1711
  class DynatraceReportingSink extends ExpandedHttpJsonReportingSink {
1508
1712
  constructor(options = {}) {
1509
1713
  super("dynatrace", "extensions.reporting_sinks.dynatrace", "/api/v2/logs/ingest", options);
1714
+ this.iterationObservationShapeLimited = true;
1510
1715
  }
1511
1716
  }
1512
1717
  exports.DynatraceReportingSink = DynatraceReportingSink;
@@ -1525,12 +1730,26 @@ exports.OpenSearchReportingSink = OpenSearchReportingSink;
1525
1730
  class NewRelicReportingSink extends ExpandedHttpJsonReportingSink {
1526
1731
  constructor(options = {}) {
1527
1732
  super("new-relic", "extensions.reporting_sinks.new_relic", "/log/v1", options);
1733
+ this.iterationObservationShapeLimited = true;
1528
1734
  }
1529
1735
  }
1530
1736
  exports.NewRelicReportingSink = NewRelicReportingSink;
1531
1737
  class GenericWebhookReportingSink extends ExpandedHttpJsonReportingSink {
1532
1738
  constructor(options = {}) {
1533
1739
  super("webhook", "extensions.reporting_sinks.webhook", "/loadstrike", options);
1740
+ this.preservesRawIterationObservations = true;
1741
+ }
1742
+ async saveIterationBatch(batch) {
1743
+ await this.persistCanonicalGzipPayload(batch);
1744
+ }
1745
+ async SaveIterationBatch(batch) {
1746
+ await this.saveIterationBatch(batch);
1747
+ }
1748
+ async completeIterationObservationStream(completion) {
1749
+ await this.persistCanonicalGzipPayload(completion);
1750
+ }
1751
+ async CompleteIterationObservationStream(completion) {
1752
+ await this.completeIterationObservationStream(completion);
1534
1753
  }
1535
1754
  }
1536
1755
  exports.GenericWebhookReportingSink = GenericWebhookReportingSink;
@@ -1542,6 +1761,12 @@ class KafkaReportingSink extends ExpandedEventReportingSink {
1542
1761
  this.staticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
1543
1762
  this.publishAsync = pickRecordValue(source, "publishAsync", "PublishAsync");
1544
1763
  }
1764
+ async saveIterationBatch(batch) {
1765
+ await this.publishCanonicalValue(batch);
1766
+ }
1767
+ async completeIterationObservationStream(completion) {
1768
+ await this.publishCanonicalValue(completion);
1769
+ }
1545
1770
  async persistEvents(events) {
1546
1771
  if (!events.length) {
1547
1772
  return;
@@ -1551,11 +1776,18 @@ class KafkaReportingSink extends ExpandedEventReportingSink {
1551
1776
  }
1552
1777
  await this.publishAsync(this.topic, JSON.stringify(this.buildPayload(events, this.staticTags)));
1553
1778
  }
1779
+ async publishCanonicalValue(value) {
1780
+ if (!this.publishAsync) {
1781
+ throw new Error("KafkaReportingSink requires PublishAsync when a Kafka producer is not configured by the host application.");
1782
+ }
1783
+ await this.publishAsync(this.topic, JSON.stringify(value));
1784
+ }
1554
1785
  }
1555
1786
  exports.KafkaReportingSink = KafkaReportingSink;
1556
1787
  class StatsDReportingSinkBase extends ExpandedEventReportingSink {
1557
1788
  constructor(sinkName, licenseFeature, options = {}, dogStatsD = false) {
1558
1789
  super(sinkName, licenseFeature);
1790
+ this.iterationObservationShapeLimited = true;
1559
1791
  const source = asRecord(options);
1560
1792
  this.prefix = optionString(source, "prefix", "Prefix").trim() || "loadstrike";
1561
1793
  this.host = optionString(source, "host", "Host").trim() || "127.0.0.1";
@@ -1564,6 +1796,47 @@ class StatsDReportingSinkBase extends ExpandedEventReportingSink {
1564
1796
  this.sendLineAsync = pickRecordValue(source, "sendLineAsync", "SendLineAsync");
1565
1797
  this.dogStatsD = dogStatsD;
1566
1798
  }
1799
+ async saveIterationBatch(batch) {
1800
+ for (const observation of batch.observations) {
1801
+ const tags = {
1802
+ run_id: batch.runId,
1803
+ result_owner_id: batch.resultOwnerId,
1804
+ scenario_name: observation.scenarioName,
1805
+ phase: observation.phase,
1806
+ outcome: observation.isSuccess ? "success" : "failure"
1807
+ };
1808
+ const occurredUtc = new Date();
1809
+ await this.sendLine(this.formatPoint({
1810
+ metricName: "iteration.reported_latency_us",
1811
+ metricKind: "gauge",
1812
+ occurredUtc,
1813
+ value: Number(observation.reportedLatencyUs64),
1814
+ unitOfMeasure: "us",
1815
+ tags
1816
+ }));
1817
+ await this.sendLine(this.formatPoint({
1818
+ metricName: "iteration.attempt",
1819
+ metricKind: "count",
1820
+ occurredUtc,
1821
+ value: 1,
1822
+ unitOfMeasure: "count",
1823
+ tags
1824
+ }));
1825
+ }
1826
+ }
1827
+ async completeIterationObservationStream(completion) {
1828
+ await this.sendLine(this.formatPoint({
1829
+ metricName: "observation.reporting_complete",
1830
+ metricKind: "gauge",
1831
+ occurredUtc: new Date(),
1832
+ value: completion.reportingComplete ? 1 : 0,
1833
+ unitOfMeasure: "boolean",
1834
+ tags: {
1835
+ run_id: completion.runId,
1836
+ result_owner_id: completion.resultOwnerId
1837
+ }
1838
+ }));
1839
+ }
1567
1840
  async persistEvents(events) {
1568
1841
  const points = createReportingSinkMetricPoints(this.sinkName, events, this.tags);
1569
1842
  for (const point of points) {
@@ -1606,6 +1879,8 @@ exports.NetdataStatsDReportingSink = NetdataStatsDReportingSink;
1606
1879
  class JsonlFileReportingSink extends ExpandedEventReportingSink {
1607
1880
  constructor(options = {}) {
1608
1881
  super("jsonl", "extensions.reporting_sinks.jsonl");
1882
+ this.preservesRawIterationObservations = true;
1883
+ this.writeTail = Promise.resolve();
1609
1884
  const source = asRecord(options);
1610
1885
  this.filePath = optionString(source, "filePath", "FilePath").trim() || "loadstrike-reporting.jsonl";
1611
1886
  this.staticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
@@ -1614,8 +1889,28 @@ class JsonlFileReportingSink extends ExpandedEventReportingSink {
1614
1889
  if (!events.length) {
1615
1890
  return;
1616
1891
  }
1617
- (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(this.filePath), { recursive: true });
1618
- (0, node_fs_1.appendFileSync)(this.filePath, `${JSON.stringify(this.buildPayload(events, this.staticTags))}\n`, "utf8");
1892
+ await this.persistCanonicalPayload(this.buildPayload(events, this.staticTags));
1893
+ }
1894
+ async saveIterationBatch(batch) {
1895
+ await this.persistCanonicalPayload(batch);
1896
+ }
1897
+ async SaveIterationBatch(batch) {
1898
+ await this.saveIterationBatch(batch);
1899
+ }
1900
+ async completeIterationObservationStream(completion) {
1901
+ await this.persistCanonicalPayload(completion);
1902
+ }
1903
+ async CompleteIterationObservationStream(completion) {
1904
+ await this.completeIterationObservationStream(completion);
1905
+ }
1906
+ persistCanonicalPayload(payload) {
1907
+ const line = `${JSON.stringify(payload)}\n`;
1908
+ const write = this.writeTail.then(async () => {
1909
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(this.filePath), { recursive: true });
1910
+ await (0, promises_1.appendFile)(this.filePath, line, "utf8");
1911
+ });
1912
+ this.writeTail = write.catch(() => undefined);
1913
+ return write;
1619
1914
  }
1620
1915
  }
1621
1916
  exports.JsonlFileReportingSink = JsonlFileReportingSink;
@@ -1663,9 +1958,18 @@ function createRunResultEvents(session, result) {
1663
1958
  completed_utc: result.completedUtc,
1664
1959
  report_file_count: result.reportFiles.length,
1665
1960
  disabled_sink_count: result.disabledSinks.length,
1666
- sink_error_count: result.sinkErrors.length
1961
+ sink_error_count: result.sinkErrors.length,
1962
+ reporting_complete: result.reportingComplete ?? true,
1963
+ observation_last_batch_sequence_64: result.observationDeliveryStats?.lastBatchSequence64 ?? "-1",
1964
+ observation_captured_count_64: result.observationDeliveryStats?.capturedCount64 ?? "0",
1965
+ observation_delivered_count_64: result.observationDeliveryStats?.deliveredCount64 ?? "0",
1966
+ observation_dropped_buffer_count_64: result.observationDeliveryStats?.droppedBufferCount64 ?? "0",
1967
+ observation_dropped_sink_count_64: result.observationDeliveryStats?.droppedSinkCount64 ?? "0"
1667
1968
  })
1668
1969
  ];
1970
+ for (const row of result.correlationRows ?? []) {
1971
+ events.push(createCorrelationOutcomeEvent(session, occurredUtc, row));
1972
+ }
1669
1973
  for (const reportFile of result.reportFiles) {
1670
1974
  events.push(createReportingEvent(session, occurredUtc, "run.report.final", null, null, {
1671
1975
  phase: "final",
@@ -1696,8 +2000,65 @@ function createRunResultEvents(session, result) {
1696
2000
  attempts: sinkError.attempts
1697
2001
  }));
1698
2002
  }
2003
+ for (const warning of result.generatorWarnings ?? []) {
2004
+ events.push(createReportingEvent(session, occurredUtc, "run.generator-warning.final", warning.scenarioName || null, null, {
2005
+ phase: "final",
2006
+ entity: "generator-warning",
2007
+ warning_code: warning.code,
2008
+ ...(warning.sinkName ? { sink_name: warning.sinkName } : {})
2009
+ }, {
2010
+ warning_code: warning.code,
2011
+ sink_name: warning.sinkName ?? "",
2012
+ scenario_name: warning.scenarioName,
2013
+ scenario_index: warning.scenarioIndex ?? -1,
2014
+ simulation_index: warning.simulationIndex,
2015
+ simulation_kind: warning.simulationKind ?? "",
2016
+ count_64: warning.count64,
2017
+ message: warning.message,
2018
+ first_observed_utc_ns: warning.firstObservedUtcNs,
2019
+ last_observed_utc_ns: warning.lastObservedUtcNs
2020
+ }));
2021
+ }
1699
2022
  return events;
1700
2023
  }
2024
+ function createCorrelationOutcomeEvent(session, fallbackOccurredUtc, row) {
2025
+ const occurredToken = optionString(row, "occurredUtc", "OccurredUtc");
2026
+ const parsedOccurredUtc = occurredToken ? new Date(occurredToken) : fallbackOccurredUtc;
2027
+ const occurredUtc = Number.isFinite(parsedOccurredUtc.getTime())
2028
+ ? parsedOccurredUtc
2029
+ : fallbackOccurredUtc;
2030
+ const scenarioName = optionString(row, "scenario", "Scenario");
2031
+ const source = optionString(row, "source", "Source");
2032
+ const destination = optionString(row, "destination", "Destination");
2033
+ const runMode = optionString(row, "runMode", "RunMode");
2034
+ const statusCode = optionString(row, "statusCode", "StatusCode");
2035
+ const gatherByField = optionString(row, "gatherByField", "GatherByField");
2036
+ const gatherByValue = optionString(row, "gatherByValue", "GatherByValue");
2037
+ return createReportingEvent(session, occurredUtc, "correlation.outcome.final", scenarioName || null, null, {
2038
+ phase: "final",
2039
+ entity: "correlation-outcome",
2040
+ source,
2041
+ destination,
2042
+ run_mode: runMode,
2043
+ status_code: statusCode,
2044
+ gather_by_field: gatherByField,
2045
+ gather_by_value: gatherByValue
2046
+ }, {
2047
+ occurred_utc: occurredToken || occurredUtc.toISOString(),
2048
+ source,
2049
+ destination,
2050
+ run_mode: runMode,
2051
+ status_code: statusCode,
2052
+ is_success: pickBooleanValue(row, false, "isSuccess", "IsSuccess"),
2053
+ is_failure: pickBooleanValue(row, false, "isFailure", "IsFailure"),
2054
+ gather_by_field: gatherByField,
2055
+ gather_by_value: gatherByValue,
2056
+ tracking_id: optionString(row, "trackingId", "TrackingId"),
2057
+ event_id: optionString(row, "eventId", "EventId"),
2058
+ latency_ms: optionNumber(row, "latencyMs", "LatencyMs") ?? 0,
2059
+ message: optionString(row, "message", "Message")
2060
+ });
2061
+ }
1701
2062
  function createNodeSummaryEvent(session, occurredUtc, stats) {
1702
2063
  return createReportingEvent(session, occurredUtc, "test.final", null, null, {
1703
2064
  phase: "final",
@@ -1883,6 +2244,125 @@ function createReportingEvent(session, occurredUtc, eventType, scenarioName, ste
1883
2244
  fields: eventFields
1884
2245
  };
1885
2246
  }
2247
+ function removeSdkPercentileFields(event) {
2248
+ const fields = Object.fromEntries(Object.entries(event.fields).filter(([name]) => !/_(?:latency|bytes)_p(?:50|75|95|99)(?:_|$)/iu.test(name)));
2249
+ return {
2250
+ ...event,
2251
+ fields
2252
+ };
2253
+ }
2254
+ function createIterationObservationEvents(session, batch) {
2255
+ const events = [];
2256
+ const occurredUtc = dateFromUtcNanoseconds(batch.createdUtcNs);
2257
+ for (const observation of batch.observations) {
2258
+ const tags = {
2259
+ run_id: batch.runId,
2260
+ result_owner_id: batch.resultOwnerId,
2261
+ phase: observation.phase,
2262
+ scenario_name: observation.scenarioName,
2263
+ simulation_kind: observation.simulationKind,
2264
+ is_final_attempt: observation.isFinalAttempt ? "true" : "false"
2265
+ };
2266
+ events.push({
2267
+ runId: batch.runId,
2268
+ eventType: "iteration.observation",
2269
+ occurredUtc,
2270
+ sessionId: batch.sessionId,
2271
+ testSuite: session.testSuite,
2272
+ testName: session.testName,
2273
+ clusterId: session.clusterId,
2274
+ nodeType: session.nodeType,
2275
+ machineName: session.machineName,
2276
+ scenarioName: observation.scenarioName,
2277
+ stepName: null,
2278
+ tags,
2279
+ fields: {
2280
+ schema_version: observation.schemaVersion,
2281
+ batch_id: batch.batchId,
2282
+ observation_id: observation.observationId,
2283
+ iteration_id: observation.iterationId,
2284
+ process_group: observation.processGroup,
2285
+ scenario_index: observation.scenarioIndex,
2286
+ simulation_index: observation.simulationIndex,
2287
+ global_ordinal_64: observation.globalOrdinal64,
2288
+ shard_index: observation.shardIndex,
2289
+ shard_count: observation.shardCount,
2290
+ attempt_index: observation.attemptIndex,
2291
+ started_utc_ns: observation.startedUtcNs,
2292
+ completed_utc_ns: observation.completedUtcNs,
2293
+ observed_latency_us_64: observation.observedLatencyUs64,
2294
+ reported_latency_us_64: observation.reportedLatencyUs64,
2295
+ is_success: observation.isSuccess,
2296
+ status_code: observation.statusCode,
2297
+ size_bytes_64: observation.sizeBytes64
2298
+ }
2299
+ });
2300
+ for (const step of observation.steps) {
2301
+ events.push({
2302
+ runId: batch.runId,
2303
+ eventType: "iteration.step",
2304
+ occurredUtc,
2305
+ sessionId: batch.sessionId,
2306
+ testSuite: session.testSuite,
2307
+ testName: session.testName,
2308
+ clusterId: session.clusterId,
2309
+ nodeType: session.nodeType,
2310
+ machineName: session.machineName,
2311
+ scenarioName: observation.scenarioName,
2312
+ stepName: step.stepName,
2313
+ tags: { ...tags },
2314
+ fields: {
2315
+ schema_version: observation.schemaVersion,
2316
+ batch_id: batch.batchId,
2317
+ observation_id: observation.observationId,
2318
+ iteration_id: observation.iterationId,
2319
+ attempt_index: observation.attemptIndex,
2320
+ sort_index: step.sortIndex,
2321
+ started_utc_ns: step.startedUtcNs,
2322
+ completed_utc_ns: step.completedUtcNs,
2323
+ observed_latency_us_64: step.observedLatencyUs64,
2324
+ reported_latency_us_64: step.reportedLatencyUs64,
2325
+ is_success: step.isSuccess,
2326
+ status_code: step.statusCode,
2327
+ size_bytes_64: step.sizeBytes64
2328
+ }
2329
+ });
2330
+ }
2331
+ }
2332
+ return events;
2333
+ }
2334
+ function createIterationCompletionEvents(session, completion) {
2335
+ const occurredUtc = dateFromUtcNanoseconds(completion.completedUtcNs);
2336
+ return [{
2337
+ runId: completion.runId,
2338
+ eventType: "observation.stream.completed",
2339
+ occurredUtc,
2340
+ sessionId: completion.sessionId,
2341
+ testSuite: session.testSuite,
2342
+ testName: session.testName,
2343
+ clusterId: session.clusterId,
2344
+ nodeType: session.nodeType,
2345
+ machineName: session.machineName,
2346
+ scenarioName: null,
2347
+ stepName: null,
2348
+ tags: {
2349
+ run_id: completion.runId,
2350
+ result_owner_id: completion.resultOwnerId,
2351
+ phase: "final"
2352
+ },
2353
+ fields: {
2354
+ schema_version: completion.schemaVersion,
2355
+ process_group: completion.processGroup,
2356
+ last_batch_sequence_64: completion.lastBatchSequence64,
2357
+ captured_count_64: completion.capturedCount64,
2358
+ delivered_count_64: completion.deliveredCount64,
2359
+ dropped_buffer_count_64: completion.droppedBufferCount64,
2360
+ dropped_sink_count_64: completion.droppedSinkCount64,
2361
+ reporting_complete: completion.reportingComplete,
2362
+ completed_utc_ns: completion.completedUtcNs
2363
+ }
2364
+ }];
2365
+ }
1886
2366
  function portalEventPayload(event, index) {
1887
2367
  return {
1888
2368
  eventId: portalEventId(event, index),
@@ -1915,6 +2395,22 @@ function portalEventId(event, index) {
1915
2395
  });
1916
2396
  return `lsr_${(0, node_crypto_1.createHash)("sha256").update(material).digest("hex").slice(0, 40)}`;
1917
2397
  }
2398
+ function dateFromUtcNanoseconds(value) {
2399
+ try {
2400
+ const nanoseconds = BigInt(value);
2401
+ if (nanoseconds < 0n) {
2402
+ return new Date(0);
2403
+ }
2404
+ const milliseconds = nanoseconds / 1000000n;
2405
+ const numeric = Number(milliseconds);
2406
+ return Number.isFinite(numeric) && numeric <= 8640000000000000
2407
+ ? new Date(numeric)
2408
+ : new Date(0);
2409
+ }
2410
+ catch {
2411
+ return new Date(0);
2412
+ }
2413
+ }
1918
2414
  function addMeasurementFields(fields, prefix, measurement) {
1919
2415
  const request = measurement?.request ?? { count: 0, percent: 0, rps: 0 };
1920
2416
  const latency = measurement?.latency ?? {
@@ -1955,10 +2451,6 @@ function addMeasurementFields(fields, prefix, measurement) {
1955
2451
  fields[`${prefix}_latency_min_ms`] = latency.minMs ?? 0;
1956
2452
  fields[`${prefix}_latency_mean_ms`] = latency.meanMs ?? 0;
1957
2453
  fields[`${prefix}_latency_max_ms`] = latency.maxMs ?? 0;
1958
- fields[`${prefix}_latency_p50_ms`] = latency.percent50 ?? 0;
1959
- fields[`${prefix}_latency_p75_ms`] = latency.percent75 ?? 0;
1960
- fields[`${prefix}_latency_p95_ms`] = latency.percent95 ?? 0;
1961
- fields[`${prefix}_latency_p99_ms`] = latency.percent99 ?? 0;
1962
2454
  fields[`${prefix}_latency_std_dev`] = latency.stdDev ?? 0;
1963
2455
  fields[`${prefix}_latency_le_800_count`] = latencyCount.lessOrEq800 ?? 0;
1964
2456
  fields[`${prefix}_latency_gt_800_lt_1200_count`] = latencyCount.more800Less1200 ?? 0;
@@ -1967,10 +2459,6 @@ function addMeasurementFields(fields, prefix, measurement) {
1967
2459
  fields[`${prefix}_bytes_min`] = dataTransfer.minBytes ?? 0;
1968
2460
  fields[`${prefix}_bytes_mean`] = dataTransfer.meanBytes ?? 0;
1969
2461
  fields[`${prefix}_bytes_max`] = dataTransfer.maxBytes ?? 0;
1970
- fields[`${prefix}_bytes_p50`] = dataTransfer.percent50 ?? 0;
1971
- fields[`${prefix}_bytes_p75`] = dataTransfer.percent75 ?? 0;
1972
- fields[`${prefix}_bytes_p95`] = dataTransfer.percent95 ?? 0;
1973
- fields[`${prefix}_bytes_p99`] = dataTransfer.percent99 ?? 0;
1974
2462
  fields[`${prefix}_bytes_std_dev`] = dataTransfer.stdDev ?? 0;
1975
2463
  fields[`${prefix}_status_code_count`] = statusCodes.length;
1976
2464
  }
@@ -3106,27 +3594,93 @@ function deepCloneValue(value) {
3106
3594
  }
3107
3595
  return value;
3108
3596
  }
3597
+ function gzipJsonRequestBody(value) {
3598
+ return Uint8Array.from((0, iteration_observations_js_1.serializeIterationObservationBatchGzipJson)(value)).buffer;
3599
+ }
3109
3600
  async function postWithTimeout(fetchImpl, url, init, timeoutMs, sinkName) {
3110
3601
  const controller = new AbortController();
3111
3602
  const timer = setTimeout(() => controller.abort(), Math.max(timeoutMs, 1));
3112
3603
  try {
3113
3604
  const response = await fetchImpl(url, { ...init, signal: controller.signal });
3114
- const body = await readResponseBodyText(response);
3115
3605
  if (!response.ok) {
3116
- throw new Error(`${sinkName} write failed with status ${response.status}: ${body}`);
3606
+ throw new ReportingSinkHttpError(sinkName, response.status, readHttpResponseStatusText(response), readHttpResponseRequestId(response));
3117
3607
  }
3118
- return body;
3608
+ return await readResponseBodyText(response);
3119
3609
  }
3120
3610
  finally {
3121
3611
  clearTimeout(timer);
3122
3612
  }
3123
3613
  }
3124
3614
  async function readResponseBodyText(response) {
3125
- const candidate = response;
3126
- if (typeof candidate.text !== "function") {
3615
+ const body = response.body;
3616
+ if (body && typeof body.getReader === "function") {
3617
+ const reader = body.getReader();
3618
+ const chunks = [];
3619
+ let bytes = 0;
3620
+ try {
3621
+ while (true) {
3622
+ const next = await reader.read();
3623
+ if (next.done)
3624
+ break;
3625
+ if (!next.value)
3626
+ continue;
3627
+ if (bytes + next.value.byteLength > MAXIMUM_HTTP_RESPONSE_BODY_BYTES) {
3628
+ await reader.cancel();
3629
+ throw new Error("Reporting sink response exceeded the bounded response limit.");
3630
+ }
3631
+ chunks.push(next.value);
3632
+ bytes += next.value.byteLength;
3633
+ }
3634
+ }
3635
+ finally {
3636
+ reader.releaseLock();
3637
+ }
3638
+ return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
3639
+ }
3640
+ return "";
3641
+ }
3642
+ function readHttpResponseStatusText(response) {
3643
+ try {
3644
+ return normalizeHttpMetadata(response.statusText);
3645
+ }
3646
+ catch {
3647
+ return "";
3648
+ }
3649
+ }
3650
+ function readHttpResponseRequestId(response) {
3651
+ for (const name of [
3652
+ "x-request-id",
3653
+ "x-correlation-id",
3654
+ "request-id",
3655
+ "traceparent"
3656
+ ]) {
3657
+ try {
3658
+ const value = response.headers?.get(name);
3659
+ if (value) {
3660
+ return normalizeHttpMetadata(value);
3661
+ }
3662
+ }
3663
+ catch {
3664
+ return "";
3665
+ }
3666
+ }
3667
+ return "";
3668
+ }
3669
+ function normalizeHttpMetadata(value) {
3670
+ let text;
3671
+ try {
3672
+ const source = String(value ?? "");
3673
+ text = source.length <= MAXIMUM_HTTP_METADATA_CHARACTERS
3674
+ ? source
3675
+ : source.slice(0, MAXIMUM_HTTP_METADATA_CHARACTERS - HTTP_METADATA_TRUNCATION_SUFFIX.length) + HTTP_METADATA_TRUNCATION_SUFFIX;
3676
+ }
3677
+ catch {
3127
3678
  return "";
3128
3679
  }
3129
- return String(await candidate.text());
3680
+ return (0, iteration_observation_diagnostics_js_1.redactIterationObservationSecrets)(Buffer.from(text, "utf8").toString("utf8")
3681
+ .replace(/[\u0000-\u001F\u007F-\u009F]/gu, " ")
3682
+ .replace(/\s{2,}/gu, " ")
3683
+ .trim());
3130
3684
  }
3131
3685
  function validatePortalIngestResponse(responseBody) {
3132
3686
  const text = String(responseBody ?? "").trim();
@@ -3179,6 +3733,8 @@ exports.__loadstrikeTestExports = {
3179
3733
  cloneScenarioStats,
3180
3734
  cloneSessionStartInfo,
3181
3735
  createFinalStatsEvents,
3736
+ createIterationCompletionEvents,
3737
+ createIterationObservationEvents,
3182
3738
  createRealtimeStatsEvents,
3183
3739
  createRunResultEvents,
3184
3740
  createReportingEvent,
@@ -3195,6 +3751,7 @@ exports.__loadstrikeTestExports = {
3195
3751
  normalizeStringMap,
3196
3752
  optionNumber,
3197
3753
  optionString,
3754
+ portalEventPayload,
3198
3755
  pickBooleanValue,
3199
3756
  pickRecordValue,
3200
3757
  postWithTimeout,