@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/esm/sinks.js CHANGED
@@ -1,9 +1,43 @@
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";
3
+ import { appendFile, mkdir } from "node:fs/promises";
4
4
  import { dirname } from "node:path";
5
5
  import { createSocket } from "node:dgram";
6
+ import { gzip } from "node:zlib";
6
7
  import { Pool } from "pg";
8
+ import { serializeIterationObservationBatchGzipJson } from "./iteration-observations.js";
9
+ import { redactIterationObservationSecrets } from "./iteration-observation-diagnostics.js";
10
+ const PORTAL_RUN_TOKEN_PROVIDER = Symbol.for("loadstrike.internal.portal-run-token-provider");
11
+ const MAXIMUM_HTTP_RESPONSE_BODY_BYTES = 256 * 1024;
12
+ const MAXIMUM_HTTP_METADATA_CHARACTERS = 256;
13
+ const HTTP_METADATA_TRUNCATION_SUFFIX = " [truncated]";
14
+ class ReportingSinkHttpError extends Error {
15
+ constructor(sinkName, status, statusText, requestId) {
16
+ const normalizedStatus = Number.isFinite(status) ? Math.max(Math.trunc(status), 0) : 0;
17
+ const normalizedStatusText = normalizeHttpMetadata(statusText);
18
+ const normalizedRequestId = normalizeHttpMetadata(requestId);
19
+ super(`${sinkName} write failed with HTTP status ${normalizedStatus}`
20
+ + (normalizedStatusText ? ` ${normalizedStatusText}` : "")
21
+ + (normalizedRequestId ? ` (requestId=${normalizedRequestId})` : "")
22
+ + ".");
23
+ this.name = "ReportingSinkHttpError";
24
+ this.status = normalizedStatus;
25
+ this.statusText = normalizedStatusText;
26
+ this.requestId = normalizedRequestId;
27
+ }
28
+ }
29
+ function gzipJsonAsync(value) {
30
+ const json = Buffer.from(JSON.stringify(value), "utf8");
31
+ return new Promise((resolve, reject) => {
32
+ gzip(json, (error, compressed) => {
33
+ if (error) {
34
+ reject(error);
35
+ return;
36
+ }
37
+ resolve(compressed);
38
+ });
39
+ });
40
+ }
7
41
  const DEFAULT_INFLUX_CONFIGURATION_SECTION_PATH = "LoadStrike:ReportingSinks:InfluxDb";
8
42
  const DEFAULT_GRAFANA_LOKI_CONFIGURATION_SECTION_PATH = "LoadStrike:ReportingSinks:GrafanaLoki";
9
43
  const DEFAULT_TIMESCALEDB_CONFIGURATION_SECTION_PATH = "LoadStrike:ReportingSinks:TimescaleDb";
@@ -364,6 +398,28 @@ export class CompositeReportingSink {
364
398
  async SaveRunResult(result) {
365
399
  await this.saveRunResult(result);
366
400
  }
401
+ async saveIterationBatch(batch) {
402
+ for (const sink of this.sinks) {
403
+ const saveIterationBatch = sink.saveIterationBatch ?? sink.SaveIterationBatch;
404
+ if (saveIterationBatch) {
405
+ await saveIterationBatch.call(sink, batch);
406
+ }
407
+ }
408
+ }
409
+ async SaveIterationBatch(batch) {
410
+ await this.saveIterationBatch(batch);
411
+ }
412
+ async completeIterationObservationStream(completion) {
413
+ for (const sink of this.sinks) {
414
+ const completeIterationObservationStream = sink.completeIterationObservationStream ?? sink.CompleteIterationObservationStream;
415
+ if (completeIterationObservationStream) {
416
+ await completeIterationObservationStream.call(sink, completion);
417
+ }
418
+ }
419
+ }
420
+ async CompleteIterationObservationStream(completion) {
421
+ await this.completeIterationObservationStream(completion);
422
+ }
367
423
  async stop() {
368
424
  for (const sink of this.sinks) {
369
425
  const stop = sink.stop ?? sink.Stop;
@@ -378,6 +434,7 @@ export class CompositeReportingSink {
378
434
  }
379
435
  export class PortalReportingSink {
380
436
  constructor(options = {}) {
437
+ this.iterationObservationPortalSink = true;
381
438
  this.sinkName = "portal";
382
439
  this.SinkName = "portal";
383
440
  this.licenseFeature = "extensions.reporting_sinks.portal";
@@ -385,6 +442,7 @@ export class PortalReportingSink {
385
442
  this.baseContext = null;
386
443
  this.session = null;
387
444
  this.runToken = "";
445
+ this.runTokenProvider = null;
388
446
  this.ingestUrl = "";
389
447
  this.optionsInput = { ...options };
390
448
  const source = asRecord(options);
@@ -401,7 +459,11 @@ export class PortalReportingSink {
401
459
  this.init(context, infraConfig);
402
460
  }
403
461
  start(session) {
404
- this.runToken = String(session.runToken ?? session.RunToken ?? "").trim();
462
+ const internalProvider = session[PORTAL_RUN_TOKEN_PROVIDER];
463
+ this.runTokenProvider = typeof internalProvider === "function"
464
+ ? internalProvider
465
+ : () => String(session.runToken ?? session.RunToken ?? "").trim();
466
+ this.runToken = this.runTokenProvider();
405
467
  this.ingestUrl = String(session.portalReportingIngestUrl ?? session.PortalReportingIngestUrl ?? "").trim();
406
468
  if (!this.runToken || !this.ingestUrl) {
407
469
  throw new Error("PortalReportingSink requires a managed portal reporting session.");
@@ -431,8 +493,27 @@ export class PortalReportingSink {
431
493
  async SaveRunResult(result) {
432
494
  await this.saveRunResult(result);
433
495
  }
496
+ async saveIterationBatch(batch) {
497
+ const compressed = await gzipJsonAsync(batch);
498
+ await this.persistObservationPayload({
499
+ compressedObservationBatches: [{
500
+ compression: "gzip-json",
501
+ payloadBase64: compressed.toString("base64")
502
+ }]
503
+ });
504
+ }
505
+ async SaveIterationBatch(batch) {
506
+ await this.saveIterationBatch(batch);
507
+ }
508
+ async completeIterationObservationStream(completion) {
509
+ await this.persistObservationPayload({ observationStreamCompletions: [completion] });
510
+ }
511
+ async CompleteIterationObservationStream(completion) {
512
+ await this.completeIterationObservationStream(completion);
513
+ }
434
514
  stop() {
435
515
  this.session = null;
516
+ this.runTokenProvider = null;
436
517
  }
437
518
  Stop() {
438
519
  this.stop();
@@ -461,12 +542,27 @@ export class PortalReportingSink {
461
542
  method: "POST",
462
543
  headers: { "Content-Type": "application/json" },
463
544
  body: JSON.stringify({
464
- runToken: this.runToken,
465
- events: events.map((event, index) => portalEventPayload(event, index))
545
+ runToken: this.currentRunToken(),
546
+ events: events.map((event, index) => portalEventPayload(removeSdkPercentileFields(event), index))
547
+ })
548
+ }, this.timeoutMs, "PortalReportingSink");
549
+ validatePortalIngestResponse(responseBody);
550
+ }
551
+ async persistObservationPayload(payload) {
552
+ const responseBody = await postWithTimeout(this.fetchImpl, this.ingestUrl, {
553
+ method: "POST",
554
+ headers: { "Content-Type": "application/json" },
555
+ body: JSON.stringify({
556
+ runToken: this.currentRunToken(),
557
+ ...payload
466
558
  })
467
559
  }, this.timeoutMs, "PortalReportingSink");
468
560
  validatePortalIngestResponse(responseBody);
469
561
  }
562
+ currentRunToken() {
563
+ const current = this.runTokenProvider?.() ?? "";
564
+ return current || this.runToken;
565
+ }
470
566
  }
471
567
  export function cloneReportingSinkForRun(sink) {
472
568
  if (sink instanceof PortalReportingSink) {
@@ -542,6 +638,18 @@ export class InfluxDbReportingSink {
542
638
  async SaveRunResult(result) {
543
639
  await this.saveRunResult(result);
544
640
  }
641
+ async saveIterationBatch(batch) {
642
+ await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
643
+ }
644
+ async SaveIterationBatch(batch) {
645
+ await this.saveIterationBatch(batch);
646
+ }
647
+ async completeIterationObservationStream(completion) {
648
+ await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
649
+ }
650
+ async CompleteIterationObservationStream(completion) {
651
+ await this.completeIterationObservationStream(completion);
652
+ }
545
653
  stop() {
546
654
  this.session = null;
547
655
  }
@@ -651,6 +759,18 @@ export class GrafanaLokiReportingSink {
651
759
  async SaveRunResult(result) {
652
760
  await this.saveRunResult(result);
653
761
  }
762
+ async saveIterationBatch(batch) {
763
+ await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
764
+ }
765
+ async SaveIterationBatch(batch) {
766
+ await this.saveIterationBatch(batch);
767
+ }
768
+ async completeIterationObservationStream(completion) {
769
+ await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
770
+ }
771
+ async CompleteIterationObservationStream(completion) {
772
+ await this.completeIterationObservationStream(completion);
773
+ }
654
774
  stop() {
655
775
  this.session = null;
656
776
  }
@@ -796,6 +916,18 @@ export class TimescaleDbReportingSink {
796
916
  async SaveRunResult(result) {
797
917
  await this.saveRunResult(result);
798
918
  }
919
+ async saveIterationBatch(batch) {
920
+ await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
921
+ }
922
+ async SaveIterationBatch(batch) {
923
+ await this.saveIterationBatch(batch);
924
+ }
925
+ async completeIterationObservationStream(completion) {
926
+ await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
927
+ }
928
+ async CompleteIterationObservationStream(completion) {
929
+ await this.completeIterationObservationStream(completion);
930
+ }
799
931
  async stop() {
800
932
  this.session = null;
801
933
  if (this.pool) {
@@ -1071,6 +1203,18 @@ export class DatadogReportingSink {
1071
1203
  async SaveRunResult(result) {
1072
1204
  await this.saveRunResult(result);
1073
1205
  }
1206
+ async saveIterationBatch(batch) {
1207
+ await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
1208
+ }
1209
+ async SaveIterationBatch(batch) {
1210
+ await this.saveIterationBatch(batch);
1211
+ }
1212
+ async completeIterationObservationStream(completion) {
1213
+ await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
1214
+ }
1215
+ async CompleteIterationObservationStream(completion) {
1216
+ await this.completeIterationObservationStream(completion);
1217
+ }
1074
1218
  stop() {
1075
1219
  this.session = null;
1076
1220
  }
@@ -1193,6 +1337,18 @@ export class SplunkReportingSink {
1193
1337
  async SaveRunResult(result) {
1194
1338
  await this.saveRunResult(result);
1195
1339
  }
1340
+ async saveIterationBatch(batch) {
1341
+ await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
1342
+ }
1343
+ async SaveIterationBatch(batch) {
1344
+ await this.saveIterationBatch(batch);
1345
+ }
1346
+ async completeIterationObservationStream(completion) {
1347
+ await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
1348
+ }
1349
+ async CompleteIterationObservationStream(completion) {
1350
+ await this.completeIterationObservationStream(completion);
1351
+ }
1196
1352
  stop() {
1197
1353
  this.session = null;
1198
1354
  }
@@ -1295,6 +1451,18 @@ export class OtelCollectorReportingSink {
1295
1451
  async SaveRunResult(result) {
1296
1452
  await this.saveRunResult(result);
1297
1453
  }
1454
+ async saveIterationBatch(batch) {
1455
+ await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
1456
+ }
1457
+ async SaveIterationBatch(batch) {
1458
+ await this.saveIterationBatch(batch);
1459
+ }
1460
+ async completeIterationObservationStream(completion) {
1461
+ await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
1462
+ }
1463
+ async CompleteIterationObservationStream(completion) {
1464
+ await this.completeIterationObservationStream(completion);
1465
+ }
1298
1466
  stop() {
1299
1467
  this.session = null;
1300
1468
  }
@@ -1371,6 +1539,7 @@ class ExpandedEventReportingSink {
1371
1539
  constructor(sinkName, licenseFeature) {
1372
1540
  this.baseContext = null;
1373
1541
  this.session = null;
1542
+ this.preservesRawIterationObservations = true;
1374
1543
  this.sinkName = sinkName;
1375
1544
  this.SinkName = sinkName;
1376
1545
  this.licenseFeature = licenseFeature;
@@ -1406,6 +1575,18 @@ class ExpandedEventReportingSink {
1406
1575
  async SaveRunResult(result) {
1407
1576
  await this.saveRunResult(result);
1408
1577
  }
1578
+ async saveIterationBatch(batch) {
1579
+ await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
1580
+ }
1581
+ async SaveIterationBatch(batch) {
1582
+ await this.saveIterationBatch(batch);
1583
+ }
1584
+ async completeIterationObservationStream(completion) {
1585
+ await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
1586
+ }
1587
+ async CompleteIterationObservationStream(completion) {
1588
+ await this.completeIterationObservationStream(completion);
1589
+ }
1409
1590
  stop() {
1410
1591
  this.session = null;
1411
1592
  }
@@ -1429,15 +1610,18 @@ class ExpandedEventReportingSink {
1429
1610
  return this.session;
1430
1611
  }
1431
1612
  buildPayload(events, staticTags = {}) {
1613
+ const projectedEvents = this.preservesRawIterationObservations
1614
+ ? events.map(removeSdkPercentileFields)
1615
+ : events;
1432
1616
  return {
1433
1617
  sinkName: this.sinkName,
1434
1618
  runId: this.getSession().runId,
1435
1619
  sessionId: this.getSession().sessionId,
1436
- events: events.map((event) => ({
1620
+ events: projectedEvents.map((event) => ({
1437
1621
  ...event,
1438
1622
  tags: { ...staticTags, ...event.tags }
1439
1623
  })),
1440
- metrics: createReportingSinkMetricPoints(this.sinkName, events, staticTags)
1624
+ metrics: createReportingSinkMetricPoints(this.sinkName, projectedEvents, staticTags)
1441
1625
  };
1442
1626
  }
1443
1627
  }
@@ -1471,20 +1655,41 @@ class ExpandedHttpJsonReportingSink extends ExpandedEventReportingSink {
1471
1655
  body: JSON.stringify(this.buildPayload(events, this.staticTags))
1472
1656
  }, this.timeoutMs, this.constructor.name);
1473
1657
  }
1658
+ async saveIterationBatch(batch) {
1659
+ await this.persistCanonicalGzipPayload(batch);
1660
+ }
1661
+ async completeIterationObservationStream(completion) {
1662
+ await this.persistCanonicalGzipPayload(completion);
1663
+ }
1664
+ async persistCanonicalGzipPayload(payload) {
1665
+ const compressed = await gzipJsonAsync(payload);
1666
+ await postWithTimeout(this.fetchImpl, `${trimTrailingSlashes(this.baseUrl)}${normalizePath(this.endpointPath)}`, {
1667
+ method: "POST",
1668
+ headers: {
1669
+ "Content-Type": "application/json",
1670
+ "Content-Encoding": "gzip",
1671
+ ...this.headers
1672
+ },
1673
+ body: Uint8Array.from(compressed).buffer
1674
+ }, this.timeoutMs, this.constructor.name);
1675
+ }
1474
1676
  }
1475
1677
  export class PrometheusRemoteWriteReportingSink extends ExpandedHttpJsonReportingSink {
1476
1678
  constructor(options = {}) {
1477
1679
  super("prometheus-remote-write", "extensions.reporting_sinks.prometheus_remote_write", "/api/v1/write", options);
1680
+ this.iterationObservationShapeLimited = true;
1478
1681
  }
1479
1682
  }
1480
1683
  export class CloudWatchReportingSink extends ExpandedHttpJsonReportingSink {
1481
1684
  constructor(options = {}) {
1482
1685
  super("cloudwatch", "extensions.reporting_sinks.cloudwatch", "/loadstrike/events", options);
1686
+ this.iterationObservationShapeLimited = true;
1483
1687
  }
1484
1688
  }
1485
1689
  export class DynatraceReportingSink extends ExpandedHttpJsonReportingSink {
1486
1690
  constructor(options = {}) {
1487
1691
  super("dynatrace", "extensions.reporting_sinks.dynatrace", "/api/v2/logs/ingest", options);
1692
+ this.iterationObservationShapeLimited = true;
1488
1693
  }
1489
1694
  }
1490
1695
  export class ElasticsearchReportingSink extends ExpandedHttpJsonReportingSink {
@@ -1500,11 +1705,25 @@ export class OpenSearchReportingSink extends ExpandedHttpJsonReportingSink {
1500
1705
  export class NewRelicReportingSink extends ExpandedHttpJsonReportingSink {
1501
1706
  constructor(options = {}) {
1502
1707
  super("new-relic", "extensions.reporting_sinks.new_relic", "/log/v1", options);
1708
+ this.iterationObservationShapeLimited = true;
1503
1709
  }
1504
1710
  }
1505
1711
  export class GenericWebhookReportingSink extends ExpandedHttpJsonReportingSink {
1506
1712
  constructor(options = {}) {
1507
1713
  super("webhook", "extensions.reporting_sinks.webhook", "/loadstrike", options);
1714
+ this.preservesRawIterationObservations = true;
1715
+ }
1716
+ async saveIterationBatch(batch) {
1717
+ await this.persistCanonicalGzipPayload(batch);
1718
+ }
1719
+ async SaveIterationBatch(batch) {
1720
+ await this.saveIterationBatch(batch);
1721
+ }
1722
+ async completeIterationObservationStream(completion) {
1723
+ await this.persistCanonicalGzipPayload(completion);
1724
+ }
1725
+ async CompleteIterationObservationStream(completion) {
1726
+ await this.completeIterationObservationStream(completion);
1508
1727
  }
1509
1728
  }
1510
1729
  export class KafkaReportingSink extends ExpandedEventReportingSink {
@@ -1515,6 +1734,12 @@ export class KafkaReportingSink extends ExpandedEventReportingSink {
1515
1734
  this.staticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
1516
1735
  this.publishAsync = pickRecordValue(source, "publishAsync", "PublishAsync");
1517
1736
  }
1737
+ async saveIterationBatch(batch) {
1738
+ await this.publishCanonicalValue(batch);
1739
+ }
1740
+ async completeIterationObservationStream(completion) {
1741
+ await this.publishCanonicalValue(completion);
1742
+ }
1518
1743
  async persistEvents(events) {
1519
1744
  if (!events.length) {
1520
1745
  return;
@@ -1524,10 +1749,17 @@ export class KafkaReportingSink extends ExpandedEventReportingSink {
1524
1749
  }
1525
1750
  await this.publishAsync(this.topic, JSON.stringify(this.buildPayload(events, this.staticTags)));
1526
1751
  }
1752
+ async publishCanonicalValue(value) {
1753
+ if (!this.publishAsync) {
1754
+ throw new Error("KafkaReportingSink requires PublishAsync when a Kafka producer is not configured by the host application.");
1755
+ }
1756
+ await this.publishAsync(this.topic, JSON.stringify(value));
1757
+ }
1527
1758
  }
1528
1759
  class StatsDReportingSinkBase extends ExpandedEventReportingSink {
1529
1760
  constructor(sinkName, licenseFeature, options = {}, dogStatsD = false) {
1530
1761
  super(sinkName, licenseFeature);
1762
+ this.iterationObservationShapeLimited = true;
1531
1763
  const source = asRecord(options);
1532
1764
  this.prefix = optionString(source, "prefix", "Prefix").trim() || "loadstrike";
1533
1765
  this.host = optionString(source, "host", "Host").trim() || "127.0.0.1";
@@ -1536,6 +1768,47 @@ class StatsDReportingSinkBase extends ExpandedEventReportingSink {
1536
1768
  this.sendLineAsync = pickRecordValue(source, "sendLineAsync", "SendLineAsync");
1537
1769
  this.dogStatsD = dogStatsD;
1538
1770
  }
1771
+ async saveIterationBatch(batch) {
1772
+ for (const observation of batch.observations) {
1773
+ const tags = {
1774
+ run_id: batch.runId,
1775
+ result_owner_id: batch.resultOwnerId,
1776
+ scenario_name: observation.scenarioName,
1777
+ phase: observation.phase,
1778
+ outcome: observation.isSuccess ? "success" : "failure"
1779
+ };
1780
+ const occurredUtc = new Date();
1781
+ await this.sendLine(this.formatPoint({
1782
+ metricName: "iteration.reported_latency_us",
1783
+ metricKind: "gauge",
1784
+ occurredUtc,
1785
+ value: Number(observation.reportedLatencyUs64),
1786
+ unitOfMeasure: "us",
1787
+ tags
1788
+ }));
1789
+ await this.sendLine(this.formatPoint({
1790
+ metricName: "iteration.attempt",
1791
+ metricKind: "count",
1792
+ occurredUtc,
1793
+ value: 1,
1794
+ unitOfMeasure: "count",
1795
+ tags
1796
+ }));
1797
+ }
1798
+ }
1799
+ async completeIterationObservationStream(completion) {
1800
+ await this.sendLine(this.formatPoint({
1801
+ metricName: "observation.reporting_complete",
1802
+ metricKind: "gauge",
1803
+ occurredUtc: new Date(),
1804
+ value: completion.reportingComplete ? 1 : 0,
1805
+ unitOfMeasure: "boolean",
1806
+ tags: {
1807
+ run_id: completion.runId,
1808
+ result_owner_id: completion.resultOwnerId
1809
+ }
1810
+ }));
1811
+ }
1539
1812
  async persistEvents(events) {
1540
1813
  const points = createReportingSinkMetricPoints(this.sinkName, events, this.tags);
1541
1814
  for (const point of points) {
@@ -1575,6 +1848,8 @@ export class NetdataStatsDReportingSink extends StatsDReportingSinkBase {
1575
1848
  export class JsonlFileReportingSink extends ExpandedEventReportingSink {
1576
1849
  constructor(options = {}) {
1577
1850
  super("jsonl", "extensions.reporting_sinks.jsonl");
1851
+ this.preservesRawIterationObservations = true;
1852
+ this.writeTail = Promise.resolve();
1578
1853
  const source = asRecord(options);
1579
1854
  this.filePath = optionString(source, "filePath", "FilePath").trim() || "loadstrike-reporting.jsonl";
1580
1855
  this.staticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
@@ -1583,8 +1858,28 @@ export class JsonlFileReportingSink extends ExpandedEventReportingSink {
1583
1858
  if (!events.length) {
1584
1859
  return;
1585
1860
  }
1586
- mkdirSync(dirname(this.filePath), { recursive: true });
1587
- appendFileSync(this.filePath, `${JSON.stringify(this.buildPayload(events, this.staticTags))}\n`, "utf8");
1861
+ await this.persistCanonicalPayload(this.buildPayload(events, this.staticTags));
1862
+ }
1863
+ async saveIterationBatch(batch) {
1864
+ await this.persistCanonicalPayload(batch);
1865
+ }
1866
+ async SaveIterationBatch(batch) {
1867
+ await this.saveIterationBatch(batch);
1868
+ }
1869
+ async completeIterationObservationStream(completion) {
1870
+ await this.persistCanonicalPayload(completion);
1871
+ }
1872
+ async CompleteIterationObservationStream(completion) {
1873
+ await this.completeIterationObservationStream(completion);
1874
+ }
1875
+ persistCanonicalPayload(payload) {
1876
+ const line = `${JSON.stringify(payload)}\n`;
1877
+ const write = this.writeTail.then(async () => {
1878
+ await mkdir(dirname(this.filePath), { recursive: true });
1879
+ await appendFile(this.filePath, line, "utf8");
1880
+ });
1881
+ this.writeTail = write.catch(() => undefined);
1882
+ return write;
1588
1883
  }
1589
1884
  }
1590
1885
  function createRealtimeStatsEvents(session, scenarios) {
@@ -1631,9 +1926,18 @@ function createRunResultEvents(session, result) {
1631
1926
  completed_utc: result.completedUtc,
1632
1927
  report_file_count: result.reportFiles.length,
1633
1928
  disabled_sink_count: result.disabledSinks.length,
1634
- sink_error_count: result.sinkErrors.length
1929
+ sink_error_count: result.sinkErrors.length,
1930
+ reporting_complete: result.reportingComplete ?? true,
1931
+ observation_last_batch_sequence_64: result.observationDeliveryStats?.lastBatchSequence64 ?? "-1",
1932
+ observation_captured_count_64: result.observationDeliveryStats?.capturedCount64 ?? "0",
1933
+ observation_delivered_count_64: result.observationDeliveryStats?.deliveredCount64 ?? "0",
1934
+ observation_dropped_buffer_count_64: result.observationDeliveryStats?.droppedBufferCount64 ?? "0",
1935
+ observation_dropped_sink_count_64: result.observationDeliveryStats?.droppedSinkCount64 ?? "0"
1635
1936
  })
1636
1937
  ];
1938
+ for (const row of result.correlationRows ?? []) {
1939
+ events.push(createCorrelationOutcomeEvent(session, occurredUtc, row));
1940
+ }
1637
1941
  for (const reportFile of result.reportFiles) {
1638
1942
  events.push(createReportingEvent(session, occurredUtc, "run.report.final", null, null, {
1639
1943
  phase: "final",
@@ -1664,8 +1968,65 @@ function createRunResultEvents(session, result) {
1664
1968
  attempts: sinkError.attempts
1665
1969
  }));
1666
1970
  }
1971
+ for (const warning of result.generatorWarnings ?? []) {
1972
+ events.push(createReportingEvent(session, occurredUtc, "run.generator-warning.final", warning.scenarioName || null, null, {
1973
+ phase: "final",
1974
+ entity: "generator-warning",
1975
+ warning_code: warning.code,
1976
+ ...(warning.sinkName ? { sink_name: warning.sinkName } : {})
1977
+ }, {
1978
+ warning_code: warning.code,
1979
+ sink_name: warning.sinkName ?? "",
1980
+ scenario_name: warning.scenarioName,
1981
+ scenario_index: warning.scenarioIndex ?? -1,
1982
+ simulation_index: warning.simulationIndex,
1983
+ simulation_kind: warning.simulationKind ?? "",
1984
+ count_64: warning.count64,
1985
+ message: warning.message,
1986
+ first_observed_utc_ns: warning.firstObservedUtcNs,
1987
+ last_observed_utc_ns: warning.lastObservedUtcNs
1988
+ }));
1989
+ }
1667
1990
  return events;
1668
1991
  }
1992
+ function createCorrelationOutcomeEvent(session, fallbackOccurredUtc, row) {
1993
+ const occurredToken = optionString(row, "occurredUtc", "OccurredUtc");
1994
+ const parsedOccurredUtc = occurredToken ? new Date(occurredToken) : fallbackOccurredUtc;
1995
+ const occurredUtc = Number.isFinite(parsedOccurredUtc.getTime())
1996
+ ? parsedOccurredUtc
1997
+ : fallbackOccurredUtc;
1998
+ const scenarioName = optionString(row, "scenario", "Scenario");
1999
+ const source = optionString(row, "source", "Source");
2000
+ const destination = optionString(row, "destination", "Destination");
2001
+ const runMode = optionString(row, "runMode", "RunMode");
2002
+ const statusCode = optionString(row, "statusCode", "StatusCode");
2003
+ const gatherByField = optionString(row, "gatherByField", "GatherByField");
2004
+ const gatherByValue = optionString(row, "gatherByValue", "GatherByValue");
2005
+ return createReportingEvent(session, occurredUtc, "correlation.outcome.final", scenarioName || null, null, {
2006
+ phase: "final",
2007
+ entity: "correlation-outcome",
2008
+ source,
2009
+ destination,
2010
+ run_mode: runMode,
2011
+ status_code: statusCode,
2012
+ gather_by_field: gatherByField,
2013
+ gather_by_value: gatherByValue
2014
+ }, {
2015
+ occurred_utc: occurredToken || occurredUtc.toISOString(),
2016
+ source,
2017
+ destination,
2018
+ run_mode: runMode,
2019
+ status_code: statusCode,
2020
+ is_success: pickBooleanValue(row, false, "isSuccess", "IsSuccess"),
2021
+ is_failure: pickBooleanValue(row, false, "isFailure", "IsFailure"),
2022
+ gather_by_field: gatherByField,
2023
+ gather_by_value: gatherByValue,
2024
+ tracking_id: optionString(row, "trackingId", "TrackingId"),
2025
+ event_id: optionString(row, "eventId", "EventId"),
2026
+ latency_ms: optionNumber(row, "latencyMs", "LatencyMs") ?? 0,
2027
+ message: optionString(row, "message", "Message")
2028
+ });
2029
+ }
1669
2030
  function createNodeSummaryEvent(session, occurredUtc, stats) {
1670
2031
  return createReportingEvent(session, occurredUtc, "test.final", null, null, {
1671
2032
  phase: "final",
@@ -1851,6 +2212,125 @@ function createReportingEvent(session, occurredUtc, eventType, scenarioName, ste
1851
2212
  fields: eventFields
1852
2213
  };
1853
2214
  }
2215
+ function removeSdkPercentileFields(event) {
2216
+ const fields = Object.fromEntries(Object.entries(event.fields).filter(([name]) => !/_(?:latency|bytes)_p(?:50|75|95|99)(?:_|$)/iu.test(name)));
2217
+ return {
2218
+ ...event,
2219
+ fields
2220
+ };
2221
+ }
2222
+ function createIterationObservationEvents(session, batch) {
2223
+ const events = [];
2224
+ const occurredUtc = dateFromUtcNanoseconds(batch.createdUtcNs);
2225
+ for (const observation of batch.observations) {
2226
+ const tags = {
2227
+ run_id: batch.runId,
2228
+ result_owner_id: batch.resultOwnerId,
2229
+ phase: observation.phase,
2230
+ scenario_name: observation.scenarioName,
2231
+ simulation_kind: observation.simulationKind,
2232
+ is_final_attempt: observation.isFinalAttempt ? "true" : "false"
2233
+ };
2234
+ events.push({
2235
+ runId: batch.runId,
2236
+ eventType: "iteration.observation",
2237
+ occurredUtc,
2238
+ sessionId: batch.sessionId,
2239
+ testSuite: session.testSuite,
2240
+ testName: session.testName,
2241
+ clusterId: session.clusterId,
2242
+ nodeType: session.nodeType,
2243
+ machineName: session.machineName,
2244
+ scenarioName: observation.scenarioName,
2245
+ stepName: null,
2246
+ tags,
2247
+ fields: {
2248
+ schema_version: observation.schemaVersion,
2249
+ batch_id: batch.batchId,
2250
+ observation_id: observation.observationId,
2251
+ iteration_id: observation.iterationId,
2252
+ process_group: observation.processGroup,
2253
+ scenario_index: observation.scenarioIndex,
2254
+ simulation_index: observation.simulationIndex,
2255
+ global_ordinal_64: observation.globalOrdinal64,
2256
+ shard_index: observation.shardIndex,
2257
+ shard_count: observation.shardCount,
2258
+ attempt_index: observation.attemptIndex,
2259
+ started_utc_ns: observation.startedUtcNs,
2260
+ completed_utc_ns: observation.completedUtcNs,
2261
+ observed_latency_us_64: observation.observedLatencyUs64,
2262
+ reported_latency_us_64: observation.reportedLatencyUs64,
2263
+ is_success: observation.isSuccess,
2264
+ status_code: observation.statusCode,
2265
+ size_bytes_64: observation.sizeBytes64
2266
+ }
2267
+ });
2268
+ for (const step of observation.steps) {
2269
+ events.push({
2270
+ runId: batch.runId,
2271
+ eventType: "iteration.step",
2272
+ occurredUtc,
2273
+ sessionId: batch.sessionId,
2274
+ testSuite: session.testSuite,
2275
+ testName: session.testName,
2276
+ clusterId: session.clusterId,
2277
+ nodeType: session.nodeType,
2278
+ machineName: session.machineName,
2279
+ scenarioName: observation.scenarioName,
2280
+ stepName: step.stepName,
2281
+ tags: { ...tags },
2282
+ fields: {
2283
+ schema_version: observation.schemaVersion,
2284
+ batch_id: batch.batchId,
2285
+ observation_id: observation.observationId,
2286
+ iteration_id: observation.iterationId,
2287
+ attempt_index: observation.attemptIndex,
2288
+ sort_index: step.sortIndex,
2289
+ started_utc_ns: step.startedUtcNs,
2290
+ completed_utc_ns: step.completedUtcNs,
2291
+ observed_latency_us_64: step.observedLatencyUs64,
2292
+ reported_latency_us_64: step.reportedLatencyUs64,
2293
+ is_success: step.isSuccess,
2294
+ status_code: step.statusCode,
2295
+ size_bytes_64: step.sizeBytes64
2296
+ }
2297
+ });
2298
+ }
2299
+ }
2300
+ return events;
2301
+ }
2302
+ function createIterationCompletionEvents(session, completion) {
2303
+ const occurredUtc = dateFromUtcNanoseconds(completion.completedUtcNs);
2304
+ return [{
2305
+ runId: completion.runId,
2306
+ eventType: "observation.stream.completed",
2307
+ occurredUtc,
2308
+ sessionId: completion.sessionId,
2309
+ testSuite: session.testSuite,
2310
+ testName: session.testName,
2311
+ clusterId: session.clusterId,
2312
+ nodeType: session.nodeType,
2313
+ machineName: session.machineName,
2314
+ scenarioName: null,
2315
+ stepName: null,
2316
+ tags: {
2317
+ run_id: completion.runId,
2318
+ result_owner_id: completion.resultOwnerId,
2319
+ phase: "final"
2320
+ },
2321
+ fields: {
2322
+ schema_version: completion.schemaVersion,
2323
+ process_group: completion.processGroup,
2324
+ last_batch_sequence_64: completion.lastBatchSequence64,
2325
+ captured_count_64: completion.capturedCount64,
2326
+ delivered_count_64: completion.deliveredCount64,
2327
+ dropped_buffer_count_64: completion.droppedBufferCount64,
2328
+ dropped_sink_count_64: completion.droppedSinkCount64,
2329
+ reporting_complete: completion.reportingComplete,
2330
+ completed_utc_ns: completion.completedUtcNs
2331
+ }
2332
+ }];
2333
+ }
1854
2334
  function portalEventPayload(event, index) {
1855
2335
  return {
1856
2336
  eventId: portalEventId(event, index),
@@ -1883,6 +2363,22 @@ function portalEventId(event, index) {
1883
2363
  });
1884
2364
  return `lsr_${createHash("sha256").update(material).digest("hex").slice(0, 40)}`;
1885
2365
  }
2366
+ function dateFromUtcNanoseconds(value) {
2367
+ try {
2368
+ const nanoseconds = BigInt(value);
2369
+ if (nanoseconds < 0n) {
2370
+ return new Date(0);
2371
+ }
2372
+ const milliseconds = nanoseconds / 1000000n;
2373
+ const numeric = Number(milliseconds);
2374
+ return Number.isFinite(numeric) && numeric <= 8640000000000000
2375
+ ? new Date(numeric)
2376
+ : new Date(0);
2377
+ }
2378
+ catch {
2379
+ return new Date(0);
2380
+ }
2381
+ }
1886
2382
  function addMeasurementFields(fields, prefix, measurement) {
1887
2383
  const request = measurement?.request ?? { count: 0, percent: 0, rps: 0 };
1888
2384
  const latency = measurement?.latency ?? {
@@ -1923,10 +2419,6 @@ function addMeasurementFields(fields, prefix, measurement) {
1923
2419
  fields[`${prefix}_latency_min_ms`] = latency.minMs ?? 0;
1924
2420
  fields[`${prefix}_latency_mean_ms`] = latency.meanMs ?? 0;
1925
2421
  fields[`${prefix}_latency_max_ms`] = latency.maxMs ?? 0;
1926
- fields[`${prefix}_latency_p50_ms`] = latency.percent50 ?? 0;
1927
- fields[`${prefix}_latency_p75_ms`] = latency.percent75 ?? 0;
1928
- fields[`${prefix}_latency_p95_ms`] = latency.percent95 ?? 0;
1929
- fields[`${prefix}_latency_p99_ms`] = latency.percent99 ?? 0;
1930
2422
  fields[`${prefix}_latency_std_dev`] = latency.stdDev ?? 0;
1931
2423
  fields[`${prefix}_latency_le_800_count`] = latencyCount.lessOrEq800 ?? 0;
1932
2424
  fields[`${prefix}_latency_gt_800_lt_1200_count`] = latencyCount.more800Less1200 ?? 0;
@@ -1935,10 +2427,6 @@ function addMeasurementFields(fields, prefix, measurement) {
1935
2427
  fields[`${prefix}_bytes_min`] = dataTransfer.minBytes ?? 0;
1936
2428
  fields[`${prefix}_bytes_mean`] = dataTransfer.meanBytes ?? 0;
1937
2429
  fields[`${prefix}_bytes_max`] = dataTransfer.maxBytes ?? 0;
1938
- fields[`${prefix}_bytes_p50`] = dataTransfer.percent50 ?? 0;
1939
- fields[`${prefix}_bytes_p75`] = dataTransfer.percent75 ?? 0;
1940
- fields[`${prefix}_bytes_p95`] = dataTransfer.percent95 ?? 0;
1941
- fields[`${prefix}_bytes_p99`] = dataTransfer.percent99 ?? 0;
1942
2430
  fields[`${prefix}_bytes_std_dev`] = dataTransfer.stdDev ?? 0;
1943
2431
  fields[`${prefix}_status_code_count`] = statusCodes.length;
1944
2432
  }
@@ -3074,27 +3562,93 @@ function deepCloneValue(value) {
3074
3562
  }
3075
3563
  return value;
3076
3564
  }
3565
+ function gzipJsonRequestBody(value) {
3566
+ return Uint8Array.from(serializeIterationObservationBatchGzipJson(value)).buffer;
3567
+ }
3077
3568
  async function postWithTimeout(fetchImpl, url, init, timeoutMs, sinkName) {
3078
3569
  const controller = new AbortController();
3079
3570
  const timer = setTimeout(() => controller.abort(), Math.max(timeoutMs, 1));
3080
3571
  try {
3081
3572
  const response = await fetchImpl(url, { ...init, signal: controller.signal });
3082
- const body = await readResponseBodyText(response);
3083
3573
  if (!response.ok) {
3084
- throw new Error(`${sinkName} write failed with status ${response.status}: ${body}`);
3574
+ throw new ReportingSinkHttpError(sinkName, response.status, readHttpResponseStatusText(response), readHttpResponseRequestId(response));
3085
3575
  }
3086
- return body;
3576
+ return await readResponseBodyText(response);
3087
3577
  }
3088
3578
  finally {
3089
3579
  clearTimeout(timer);
3090
3580
  }
3091
3581
  }
3092
3582
  async function readResponseBodyText(response) {
3093
- const candidate = response;
3094
- if (typeof candidate.text !== "function") {
3583
+ const body = response.body;
3584
+ if (body && typeof body.getReader === "function") {
3585
+ const reader = body.getReader();
3586
+ const chunks = [];
3587
+ let bytes = 0;
3588
+ try {
3589
+ while (true) {
3590
+ const next = await reader.read();
3591
+ if (next.done)
3592
+ break;
3593
+ if (!next.value)
3594
+ continue;
3595
+ if (bytes + next.value.byteLength > MAXIMUM_HTTP_RESPONSE_BODY_BYTES) {
3596
+ await reader.cancel();
3597
+ throw new Error("Reporting sink response exceeded the bounded response limit.");
3598
+ }
3599
+ chunks.push(next.value);
3600
+ bytes += next.value.byteLength;
3601
+ }
3602
+ }
3603
+ finally {
3604
+ reader.releaseLock();
3605
+ }
3606
+ return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
3607
+ }
3608
+ return "";
3609
+ }
3610
+ function readHttpResponseStatusText(response) {
3611
+ try {
3612
+ return normalizeHttpMetadata(response.statusText);
3613
+ }
3614
+ catch {
3615
+ return "";
3616
+ }
3617
+ }
3618
+ function readHttpResponseRequestId(response) {
3619
+ for (const name of [
3620
+ "x-request-id",
3621
+ "x-correlation-id",
3622
+ "request-id",
3623
+ "traceparent"
3624
+ ]) {
3625
+ try {
3626
+ const value = response.headers?.get(name);
3627
+ if (value) {
3628
+ return normalizeHttpMetadata(value);
3629
+ }
3630
+ }
3631
+ catch {
3632
+ return "";
3633
+ }
3634
+ }
3635
+ return "";
3636
+ }
3637
+ function normalizeHttpMetadata(value) {
3638
+ let text;
3639
+ try {
3640
+ const source = String(value ?? "");
3641
+ text = source.length <= MAXIMUM_HTTP_METADATA_CHARACTERS
3642
+ ? source
3643
+ : source.slice(0, MAXIMUM_HTTP_METADATA_CHARACTERS - HTTP_METADATA_TRUNCATION_SUFFIX.length) + HTTP_METADATA_TRUNCATION_SUFFIX;
3644
+ }
3645
+ catch {
3095
3646
  return "";
3096
3647
  }
3097
- return String(await candidate.text());
3648
+ return redactIterationObservationSecrets(Buffer.from(text, "utf8").toString("utf8")
3649
+ .replace(/[\u0000-\u001F\u007F-\u009F]/gu, " ")
3650
+ .replace(/\s{2,}/gu, " ")
3651
+ .trim());
3098
3652
  }
3099
3653
  function validatePortalIngestResponse(responseBody) {
3100
3654
  const text = String(responseBody ?? "").trim();
@@ -3147,6 +3701,8 @@ export const __loadstrikeTestExports = {
3147
3701
  cloneScenarioStats,
3148
3702
  cloneSessionStartInfo,
3149
3703
  createFinalStatsEvents,
3704
+ createIterationCompletionEvents,
3705
+ createIterationObservationEvents,
3150
3706
  createRealtimeStatsEvents,
3151
3707
  createRunResultEvents,
3152
3708
  createReportingEvent,
@@ -3163,6 +3719,7 @@ export const __loadstrikeTestExports = {
3163
3719
  normalizeStringMap,
3164
3720
  optionNumber,
3165
3721
  optionString,
3722
+ portalEventPayload,
3166
3723
  pickBooleanValue,
3167
3724
  pickRecordValue,
3168
3725
  postWithTimeout,