@loadstrike/loadstrike-sdk 1.0.30001 → 1.0.30401

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,24 @@
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
+ const PORTAL_RUN_TOKEN_PROVIDER = Symbol.for("loadstrike.internal.portal-run-token-provider");
10
+ function gzipJsonAsync(value) {
11
+ const json = Buffer.from(JSON.stringify(value), "utf8");
12
+ return new Promise((resolve, reject) => {
13
+ gzip(json, (error, compressed) => {
14
+ if (error) {
15
+ reject(error);
16
+ return;
17
+ }
18
+ resolve(compressed);
19
+ });
20
+ });
21
+ }
7
22
  const DEFAULT_INFLUX_CONFIGURATION_SECTION_PATH = "LoadStrike:ReportingSinks:InfluxDb";
8
23
  const DEFAULT_GRAFANA_LOKI_CONFIGURATION_SECTION_PATH = "LoadStrike:ReportingSinks:GrafanaLoki";
9
24
  const DEFAULT_TIMESCALEDB_CONFIGURATION_SECTION_PATH = "LoadStrike:ReportingSinks:TimescaleDb";
@@ -364,6 +379,28 @@ export class CompositeReportingSink {
364
379
  async SaveRunResult(result) {
365
380
  await this.saveRunResult(result);
366
381
  }
382
+ async saveIterationBatch(batch) {
383
+ for (const sink of this.sinks) {
384
+ const saveIterationBatch = sink.saveIterationBatch ?? sink.SaveIterationBatch;
385
+ if (saveIterationBatch) {
386
+ await saveIterationBatch.call(sink, batch);
387
+ }
388
+ }
389
+ }
390
+ async SaveIterationBatch(batch) {
391
+ await this.saveIterationBatch(batch);
392
+ }
393
+ async completeIterationObservationStream(completion) {
394
+ for (const sink of this.sinks) {
395
+ const completeIterationObservationStream = sink.completeIterationObservationStream ?? sink.CompleteIterationObservationStream;
396
+ if (completeIterationObservationStream) {
397
+ await completeIterationObservationStream.call(sink, completion);
398
+ }
399
+ }
400
+ }
401
+ async CompleteIterationObservationStream(completion) {
402
+ await this.completeIterationObservationStream(completion);
403
+ }
367
404
  async stop() {
368
405
  for (const sink of this.sinks) {
369
406
  const stop = sink.stop ?? sink.Stop;
@@ -378,6 +415,7 @@ export class CompositeReportingSink {
378
415
  }
379
416
  export class PortalReportingSink {
380
417
  constructor(options = {}) {
418
+ this.iterationObservationPortalSink = true;
381
419
  this.sinkName = "portal";
382
420
  this.SinkName = "portal";
383
421
  this.licenseFeature = "extensions.reporting_sinks.portal";
@@ -385,6 +423,7 @@ export class PortalReportingSink {
385
423
  this.baseContext = null;
386
424
  this.session = null;
387
425
  this.runToken = "";
426
+ this.runTokenProvider = null;
388
427
  this.ingestUrl = "";
389
428
  this.optionsInput = { ...options };
390
429
  const source = asRecord(options);
@@ -401,7 +440,11 @@ export class PortalReportingSink {
401
440
  this.init(context, infraConfig);
402
441
  }
403
442
  start(session) {
404
- this.runToken = String(session.runToken ?? session.RunToken ?? "").trim();
443
+ const internalProvider = session[PORTAL_RUN_TOKEN_PROVIDER];
444
+ this.runTokenProvider = typeof internalProvider === "function"
445
+ ? internalProvider
446
+ : () => String(session.runToken ?? session.RunToken ?? "").trim();
447
+ this.runToken = this.runTokenProvider();
405
448
  this.ingestUrl = String(session.portalReportingIngestUrl ?? session.PortalReportingIngestUrl ?? "").trim();
406
449
  if (!this.runToken || !this.ingestUrl) {
407
450
  throw new Error("PortalReportingSink requires a managed portal reporting session.");
@@ -431,8 +474,27 @@ export class PortalReportingSink {
431
474
  async SaveRunResult(result) {
432
475
  await this.saveRunResult(result);
433
476
  }
477
+ async saveIterationBatch(batch) {
478
+ const compressed = await gzipJsonAsync(batch);
479
+ await this.persistObservationPayload({
480
+ compressedObservationBatches: [{
481
+ compression: "gzip-json",
482
+ payloadBase64: compressed.toString("base64")
483
+ }]
484
+ });
485
+ }
486
+ async SaveIterationBatch(batch) {
487
+ await this.saveIterationBatch(batch);
488
+ }
489
+ async completeIterationObservationStream(completion) {
490
+ await this.persistObservationPayload({ observationStreamCompletions: [completion] });
491
+ }
492
+ async CompleteIterationObservationStream(completion) {
493
+ await this.completeIterationObservationStream(completion);
494
+ }
434
495
  stop() {
435
496
  this.session = null;
497
+ this.runTokenProvider = null;
436
498
  }
437
499
  Stop() {
438
500
  this.stop();
@@ -461,12 +523,27 @@ export class PortalReportingSink {
461
523
  method: "POST",
462
524
  headers: { "Content-Type": "application/json" },
463
525
  body: JSON.stringify({
464
- runToken: this.runToken,
465
- events: events.map((event, index) => portalEventPayload(event, index))
526
+ runToken: this.currentRunToken(),
527
+ events: events.map((event, index) => portalEventPayload(removeSdkPercentileFields(event), index))
528
+ })
529
+ }, this.timeoutMs, "PortalReportingSink");
530
+ validatePortalIngestResponse(responseBody);
531
+ }
532
+ async persistObservationPayload(payload) {
533
+ const responseBody = await postWithTimeout(this.fetchImpl, this.ingestUrl, {
534
+ method: "POST",
535
+ headers: { "Content-Type": "application/json" },
536
+ body: JSON.stringify({
537
+ runToken: this.currentRunToken(),
538
+ ...payload
466
539
  })
467
540
  }, this.timeoutMs, "PortalReportingSink");
468
541
  validatePortalIngestResponse(responseBody);
469
542
  }
543
+ currentRunToken() {
544
+ const current = this.runTokenProvider?.() ?? "";
545
+ return current || this.runToken;
546
+ }
470
547
  }
471
548
  export function cloneReportingSinkForRun(sink) {
472
549
  if (sink instanceof PortalReportingSink) {
@@ -542,6 +619,18 @@ export class InfluxDbReportingSink {
542
619
  async SaveRunResult(result) {
543
620
  await this.saveRunResult(result);
544
621
  }
622
+ async saveIterationBatch(batch) {
623
+ await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
624
+ }
625
+ async SaveIterationBatch(batch) {
626
+ await this.saveIterationBatch(batch);
627
+ }
628
+ async completeIterationObservationStream(completion) {
629
+ await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
630
+ }
631
+ async CompleteIterationObservationStream(completion) {
632
+ await this.completeIterationObservationStream(completion);
633
+ }
545
634
  stop() {
546
635
  this.session = null;
547
636
  }
@@ -651,6 +740,18 @@ export class GrafanaLokiReportingSink {
651
740
  async SaveRunResult(result) {
652
741
  await this.saveRunResult(result);
653
742
  }
743
+ async saveIterationBatch(batch) {
744
+ await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
745
+ }
746
+ async SaveIterationBatch(batch) {
747
+ await this.saveIterationBatch(batch);
748
+ }
749
+ async completeIterationObservationStream(completion) {
750
+ await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
751
+ }
752
+ async CompleteIterationObservationStream(completion) {
753
+ await this.completeIterationObservationStream(completion);
754
+ }
654
755
  stop() {
655
756
  this.session = null;
656
757
  }
@@ -796,6 +897,18 @@ export class TimescaleDbReportingSink {
796
897
  async SaveRunResult(result) {
797
898
  await this.saveRunResult(result);
798
899
  }
900
+ async saveIterationBatch(batch) {
901
+ await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
902
+ }
903
+ async SaveIterationBatch(batch) {
904
+ await this.saveIterationBatch(batch);
905
+ }
906
+ async completeIterationObservationStream(completion) {
907
+ await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
908
+ }
909
+ async CompleteIterationObservationStream(completion) {
910
+ await this.completeIterationObservationStream(completion);
911
+ }
799
912
  async stop() {
800
913
  this.session = null;
801
914
  if (this.pool) {
@@ -1071,6 +1184,18 @@ export class DatadogReportingSink {
1071
1184
  async SaveRunResult(result) {
1072
1185
  await this.saveRunResult(result);
1073
1186
  }
1187
+ async saveIterationBatch(batch) {
1188
+ await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
1189
+ }
1190
+ async SaveIterationBatch(batch) {
1191
+ await this.saveIterationBatch(batch);
1192
+ }
1193
+ async completeIterationObservationStream(completion) {
1194
+ await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
1195
+ }
1196
+ async CompleteIterationObservationStream(completion) {
1197
+ await this.completeIterationObservationStream(completion);
1198
+ }
1074
1199
  stop() {
1075
1200
  this.session = null;
1076
1201
  }
@@ -1193,6 +1318,18 @@ export class SplunkReportingSink {
1193
1318
  async SaveRunResult(result) {
1194
1319
  await this.saveRunResult(result);
1195
1320
  }
1321
+ async saveIterationBatch(batch) {
1322
+ await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
1323
+ }
1324
+ async SaveIterationBatch(batch) {
1325
+ await this.saveIterationBatch(batch);
1326
+ }
1327
+ async completeIterationObservationStream(completion) {
1328
+ await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
1329
+ }
1330
+ async CompleteIterationObservationStream(completion) {
1331
+ await this.completeIterationObservationStream(completion);
1332
+ }
1196
1333
  stop() {
1197
1334
  this.session = null;
1198
1335
  }
@@ -1295,6 +1432,18 @@ export class OtelCollectorReportingSink {
1295
1432
  async SaveRunResult(result) {
1296
1433
  await this.saveRunResult(result);
1297
1434
  }
1435
+ async saveIterationBatch(batch) {
1436
+ await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
1437
+ }
1438
+ async SaveIterationBatch(batch) {
1439
+ await this.saveIterationBatch(batch);
1440
+ }
1441
+ async completeIterationObservationStream(completion) {
1442
+ await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
1443
+ }
1444
+ async CompleteIterationObservationStream(completion) {
1445
+ await this.completeIterationObservationStream(completion);
1446
+ }
1298
1447
  stop() {
1299
1448
  this.session = null;
1300
1449
  }
@@ -1371,6 +1520,7 @@ class ExpandedEventReportingSink {
1371
1520
  constructor(sinkName, licenseFeature) {
1372
1521
  this.baseContext = null;
1373
1522
  this.session = null;
1523
+ this.preservesRawIterationObservations = true;
1374
1524
  this.sinkName = sinkName;
1375
1525
  this.SinkName = sinkName;
1376
1526
  this.licenseFeature = licenseFeature;
@@ -1406,6 +1556,18 @@ class ExpandedEventReportingSink {
1406
1556
  async SaveRunResult(result) {
1407
1557
  await this.saveRunResult(result);
1408
1558
  }
1559
+ async saveIterationBatch(batch) {
1560
+ await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
1561
+ }
1562
+ async SaveIterationBatch(batch) {
1563
+ await this.saveIterationBatch(batch);
1564
+ }
1565
+ async completeIterationObservationStream(completion) {
1566
+ await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
1567
+ }
1568
+ async CompleteIterationObservationStream(completion) {
1569
+ await this.completeIterationObservationStream(completion);
1570
+ }
1409
1571
  stop() {
1410
1572
  this.session = null;
1411
1573
  }
@@ -1429,15 +1591,18 @@ class ExpandedEventReportingSink {
1429
1591
  return this.session;
1430
1592
  }
1431
1593
  buildPayload(events, staticTags = {}) {
1594
+ const projectedEvents = this.preservesRawIterationObservations
1595
+ ? events.map(removeSdkPercentileFields)
1596
+ : events;
1432
1597
  return {
1433
1598
  sinkName: this.sinkName,
1434
1599
  runId: this.getSession().runId,
1435
1600
  sessionId: this.getSession().sessionId,
1436
- events: events.map((event) => ({
1601
+ events: projectedEvents.map((event) => ({
1437
1602
  ...event,
1438
1603
  tags: { ...staticTags, ...event.tags }
1439
1604
  })),
1440
- metrics: createReportingSinkMetricPoints(this.sinkName, events, staticTags)
1605
+ metrics: createReportingSinkMetricPoints(this.sinkName, projectedEvents, staticTags)
1441
1606
  };
1442
1607
  }
1443
1608
  }
@@ -1471,20 +1636,41 @@ class ExpandedHttpJsonReportingSink extends ExpandedEventReportingSink {
1471
1636
  body: JSON.stringify(this.buildPayload(events, this.staticTags))
1472
1637
  }, this.timeoutMs, this.constructor.name);
1473
1638
  }
1639
+ async saveIterationBatch(batch) {
1640
+ await this.persistCanonicalGzipPayload(batch);
1641
+ }
1642
+ async completeIterationObservationStream(completion) {
1643
+ await this.persistCanonicalGzipPayload(completion);
1644
+ }
1645
+ async persistCanonicalGzipPayload(payload) {
1646
+ const compressed = await gzipJsonAsync(payload);
1647
+ await postWithTimeout(this.fetchImpl, `${trimTrailingSlashes(this.baseUrl)}${normalizePath(this.endpointPath)}`, {
1648
+ method: "POST",
1649
+ headers: {
1650
+ "Content-Type": "application/json",
1651
+ "Content-Encoding": "gzip",
1652
+ ...this.headers
1653
+ },
1654
+ body: Uint8Array.from(compressed).buffer
1655
+ }, this.timeoutMs, this.constructor.name);
1656
+ }
1474
1657
  }
1475
1658
  export class PrometheusRemoteWriteReportingSink extends ExpandedHttpJsonReportingSink {
1476
1659
  constructor(options = {}) {
1477
1660
  super("prometheus-remote-write", "extensions.reporting_sinks.prometheus_remote_write", "/api/v1/write", options);
1661
+ this.iterationObservationShapeLimited = true;
1478
1662
  }
1479
1663
  }
1480
1664
  export class CloudWatchReportingSink extends ExpandedHttpJsonReportingSink {
1481
1665
  constructor(options = {}) {
1482
1666
  super("cloudwatch", "extensions.reporting_sinks.cloudwatch", "/loadstrike/events", options);
1667
+ this.iterationObservationShapeLimited = true;
1483
1668
  }
1484
1669
  }
1485
1670
  export class DynatraceReportingSink extends ExpandedHttpJsonReportingSink {
1486
1671
  constructor(options = {}) {
1487
1672
  super("dynatrace", "extensions.reporting_sinks.dynatrace", "/api/v2/logs/ingest", options);
1673
+ this.iterationObservationShapeLimited = true;
1488
1674
  }
1489
1675
  }
1490
1676
  export class ElasticsearchReportingSink extends ExpandedHttpJsonReportingSink {
@@ -1500,11 +1686,25 @@ export class OpenSearchReportingSink extends ExpandedHttpJsonReportingSink {
1500
1686
  export class NewRelicReportingSink extends ExpandedHttpJsonReportingSink {
1501
1687
  constructor(options = {}) {
1502
1688
  super("new-relic", "extensions.reporting_sinks.new_relic", "/log/v1", options);
1689
+ this.iterationObservationShapeLimited = true;
1503
1690
  }
1504
1691
  }
1505
1692
  export class GenericWebhookReportingSink extends ExpandedHttpJsonReportingSink {
1506
1693
  constructor(options = {}) {
1507
1694
  super("webhook", "extensions.reporting_sinks.webhook", "/loadstrike", options);
1695
+ this.preservesRawIterationObservations = true;
1696
+ }
1697
+ async saveIterationBatch(batch) {
1698
+ await this.persistCanonicalGzipPayload(batch);
1699
+ }
1700
+ async SaveIterationBatch(batch) {
1701
+ await this.saveIterationBatch(batch);
1702
+ }
1703
+ async completeIterationObservationStream(completion) {
1704
+ await this.persistCanonicalGzipPayload(completion);
1705
+ }
1706
+ async CompleteIterationObservationStream(completion) {
1707
+ await this.completeIterationObservationStream(completion);
1508
1708
  }
1509
1709
  }
1510
1710
  export class KafkaReportingSink extends ExpandedEventReportingSink {
@@ -1515,6 +1715,12 @@ export class KafkaReportingSink extends ExpandedEventReportingSink {
1515
1715
  this.staticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
1516
1716
  this.publishAsync = pickRecordValue(source, "publishAsync", "PublishAsync");
1517
1717
  }
1718
+ async saveIterationBatch(batch) {
1719
+ await this.publishCanonicalValue(batch);
1720
+ }
1721
+ async completeIterationObservationStream(completion) {
1722
+ await this.publishCanonicalValue(completion);
1723
+ }
1518
1724
  async persistEvents(events) {
1519
1725
  if (!events.length) {
1520
1726
  return;
@@ -1524,10 +1730,17 @@ export class KafkaReportingSink extends ExpandedEventReportingSink {
1524
1730
  }
1525
1731
  await this.publishAsync(this.topic, JSON.stringify(this.buildPayload(events, this.staticTags)));
1526
1732
  }
1733
+ async publishCanonicalValue(value) {
1734
+ if (!this.publishAsync) {
1735
+ throw new Error("KafkaReportingSink requires PublishAsync when a Kafka producer is not configured by the host application.");
1736
+ }
1737
+ await this.publishAsync(this.topic, JSON.stringify(value));
1738
+ }
1527
1739
  }
1528
1740
  class StatsDReportingSinkBase extends ExpandedEventReportingSink {
1529
1741
  constructor(sinkName, licenseFeature, options = {}, dogStatsD = false) {
1530
1742
  super(sinkName, licenseFeature);
1743
+ this.iterationObservationShapeLimited = true;
1531
1744
  const source = asRecord(options);
1532
1745
  this.prefix = optionString(source, "prefix", "Prefix").trim() || "loadstrike";
1533
1746
  this.host = optionString(source, "host", "Host").trim() || "127.0.0.1";
@@ -1536,6 +1749,47 @@ class StatsDReportingSinkBase extends ExpandedEventReportingSink {
1536
1749
  this.sendLineAsync = pickRecordValue(source, "sendLineAsync", "SendLineAsync");
1537
1750
  this.dogStatsD = dogStatsD;
1538
1751
  }
1752
+ async saveIterationBatch(batch) {
1753
+ for (const observation of batch.observations) {
1754
+ const tags = {
1755
+ run_id: batch.runId,
1756
+ result_owner_id: batch.resultOwnerId,
1757
+ scenario_name: observation.scenarioName,
1758
+ phase: observation.phase,
1759
+ outcome: observation.isSuccess ? "success" : "failure"
1760
+ };
1761
+ const occurredUtc = new Date();
1762
+ await this.sendLine(this.formatPoint({
1763
+ metricName: "iteration.reported_latency_us",
1764
+ metricKind: "gauge",
1765
+ occurredUtc,
1766
+ value: Number(observation.reportedLatencyUs64),
1767
+ unitOfMeasure: "us",
1768
+ tags
1769
+ }));
1770
+ await this.sendLine(this.formatPoint({
1771
+ metricName: "iteration.attempt",
1772
+ metricKind: "count",
1773
+ occurredUtc,
1774
+ value: 1,
1775
+ unitOfMeasure: "count",
1776
+ tags
1777
+ }));
1778
+ }
1779
+ }
1780
+ async completeIterationObservationStream(completion) {
1781
+ await this.sendLine(this.formatPoint({
1782
+ metricName: "observation.reporting_complete",
1783
+ metricKind: "gauge",
1784
+ occurredUtc: new Date(),
1785
+ value: completion.reportingComplete ? 1 : 0,
1786
+ unitOfMeasure: "boolean",
1787
+ tags: {
1788
+ run_id: completion.runId,
1789
+ result_owner_id: completion.resultOwnerId
1790
+ }
1791
+ }));
1792
+ }
1539
1793
  async persistEvents(events) {
1540
1794
  const points = createReportingSinkMetricPoints(this.sinkName, events, this.tags);
1541
1795
  for (const point of points) {
@@ -1575,6 +1829,8 @@ export class NetdataStatsDReportingSink extends StatsDReportingSinkBase {
1575
1829
  export class JsonlFileReportingSink extends ExpandedEventReportingSink {
1576
1830
  constructor(options = {}) {
1577
1831
  super("jsonl", "extensions.reporting_sinks.jsonl");
1832
+ this.preservesRawIterationObservations = true;
1833
+ this.writeTail = Promise.resolve();
1578
1834
  const source = asRecord(options);
1579
1835
  this.filePath = optionString(source, "filePath", "FilePath").trim() || "loadstrike-reporting.jsonl";
1580
1836
  this.staticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
@@ -1583,8 +1839,28 @@ export class JsonlFileReportingSink extends ExpandedEventReportingSink {
1583
1839
  if (!events.length) {
1584
1840
  return;
1585
1841
  }
1586
- mkdirSync(dirname(this.filePath), { recursive: true });
1587
- appendFileSync(this.filePath, `${JSON.stringify(this.buildPayload(events, this.staticTags))}\n`, "utf8");
1842
+ await this.persistCanonicalPayload(this.buildPayload(events, this.staticTags));
1843
+ }
1844
+ async saveIterationBatch(batch) {
1845
+ await this.persistCanonicalPayload(batch);
1846
+ }
1847
+ async SaveIterationBatch(batch) {
1848
+ await this.saveIterationBatch(batch);
1849
+ }
1850
+ async completeIterationObservationStream(completion) {
1851
+ await this.persistCanonicalPayload(completion);
1852
+ }
1853
+ async CompleteIterationObservationStream(completion) {
1854
+ await this.completeIterationObservationStream(completion);
1855
+ }
1856
+ persistCanonicalPayload(payload) {
1857
+ const line = `${JSON.stringify(payload)}\n`;
1858
+ const write = this.writeTail.then(async () => {
1859
+ await mkdir(dirname(this.filePath), { recursive: true });
1860
+ await appendFile(this.filePath, line, "utf8");
1861
+ });
1862
+ this.writeTail = write.catch(() => undefined);
1863
+ return write;
1588
1864
  }
1589
1865
  }
1590
1866
  function createRealtimeStatsEvents(session, scenarios) {
@@ -1631,9 +1907,18 @@ function createRunResultEvents(session, result) {
1631
1907
  completed_utc: result.completedUtc,
1632
1908
  report_file_count: result.reportFiles.length,
1633
1909
  disabled_sink_count: result.disabledSinks.length,
1634
- sink_error_count: result.sinkErrors.length
1910
+ sink_error_count: result.sinkErrors.length,
1911
+ reporting_complete: result.reportingComplete ?? true,
1912
+ observation_last_batch_sequence_64: result.observationDeliveryStats?.lastBatchSequence64 ?? "-1",
1913
+ observation_captured_count_64: result.observationDeliveryStats?.capturedCount64 ?? "0",
1914
+ observation_delivered_count_64: result.observationDeliveryStats?.deliveredCount64 ?? "0",
1915
+ observation_dropped_buffer_count_64: result.observationDeliveryStats?.droppedBufferCount64 ?? "0",
1916
+ observation_dropped_sink_count_64: result.observationDeliveryStats?.droppedSinkCount64 ?? "0"
1635
1917
  })
1636
1918
  ];
1919
+ for (const row of result.correlationRows ?? []) {
1920
+ events.push(createCorrelationOutcomeEvent(session, occurredUtc, row));
1921
+ }
1637
1922
  for (const reportFile of result.reportFiles) {
1638
1923
  events.push(createReportingEvent(session, occurredUtc, "run.report.final", null, null, {
1639
1924
  phase: "final",
@@ -1664,8 +1949,65 @@ function createRunResultEvents(session, result) {
1664
1949
  attempts: sinkError.attempts
1665
1950
  }));
1666
1951
  }
1952
+ for (const warning of result.generatorWarnings ?? []) {
1953
+ events.push(createReportingEvent(session, occurredUtc, "run.generator-warning.final", warning.scenarioName || null, null, {
1954
+ phase: "final",
1955
+ entity: "generator-warning",
1956
+ warning_code: warning.code,
1957
+ ...(warning.sinkName ? { sink_name: warning.sinkName } : {})
1958
+ }, {
1959
+ warning_code: warning.code,
1960
+ sink_name: warning.sinkName ?? "",
1961
+ scenario_name: warning.scenarioName,
1962
+ scenario_index: warning.scenarioIndex ?? -1,
1963
+ simulation_index: warning.simulationIndex,
1964
+ simulation_kind: warning.simulationKind ?? "",
1965
+ count_64: warning.count64,
1966
+ message: warning.message,
1967
+ first_observed_utc_ns: warning.firstObservedUtcNs,
1968
+ last_observed_utc_ns: warning.lastObservedUtcNs
1969
+ }));
1970
+ }
1667
1971
  return events;
1668
1972
  }
1973
+ function createCorrelationOutcomeEvent(session, fallbackOccurredUtc, row) {
1974
+ const occurredToken = optionString(row, "occurredUtc", "OccurredUtc");
1975
+ const parsedOccurredUtc = occurredToken ? new Date(occurredToken) : fallbackOccurredUtc;
1976
+ const occurredUtc = Number.isFinite(parsedOccurredUtc.getTime())
1977
+ ? parsedOccurredUtc
1978
+ : fallbackOccurredUtc;
1979
+ const scenarioName = optionString(row, "scenario", "Scenario");
1980
+ const source = optionString(row, "source", "Source");
1981
+ const destination = optionString(row, "destination", "Destination");
1982
+ const runMode = optionString(row, "runMode", "RunMode");
1983
+ const statusCode = optionString(row, "statusCode", "StatusCode");
1984
+ const gatherByField = optionString(row, "gatherByField", "GatherByField");
1985
+ const gatherByValue = optionString(row, "gatherByValue", "GatherByValue");
1986
+ return createReportingEvent(session, occurredUtc, "correlation.outcome.final", scenarioName || null, null, {
1987
+ phase: "final",
1988
+ entity: "correlation-outcome",
1989
+ source,
1990
+ destination,
1991
+ run_mode: runMode,
1992
+ status_code: statusCode,
1993
+ gather_by_field: gatherByField,
1994
+ gather_by_value: gatherByValue
1995
+ }, {
1996
+ occurred_utc: occurredToken || occurredUtc.toISOString(),
1997
+ source,
1998
+ destination,
1999
+ run_mode: runMode,
2000
+ status_code: statusCode,
2001
+ is_success: pickBooleanValue(row, false, "isSuccess", "IsSuccess"),
2002
+ is_failure: pickBooleanValue(row, false, "isFailure", "IsFailure"),
2003
+ gather_by_field: gatherByField,
2004
+ gather_by_value: gatherByValue,
2005
+ tracking_id: optionString(row, "trackingId", "TrackingId"),
2006
+ event_id: optionString(row, "eventId", "EventId"),
2007
+ latency_ms: optionNumber(row, "latencyMs", "LatencyMs") ?? 0,
2008
+ message: optionString(row, "message", "Message")
2009
+ });
2010
+ }
1669
2011
  function createNodeSummaryEvent(session, occurredUtc, stats) {
1670
2012
  return createReportingEvent(session, occurredUtc, "test.final", null, null, {
1671
2013
  phase: "final",
@@ -1851,6 +2193,123 @@ function createReportingEvent(session, occurredUtc, eventType, scenarioName, ste
1851
2193
  fields: eventFields
1852
2194
  };
1853
2195
  }
2196
+ function removeSdkPercentileFields(event) {
2197
+ const fields = Object.fromEntries(Object.entries(event.fields).filter(([name]) => !/_(?:latency|bytes)_p(?:50|75|95|99)(?:_|$)/iu.test(name)));
2198
+ return {
2199
+ ...event,
2200
+ fields
2201
+ };
2202
+ }
2203
+ function createIterationObservationEvents(session, batch) {
2204
+ const events = [];
2205
+ for (const observation of batch.observations) {
2206
+ const tags = {
2207
+ run_id: batch.runId,
2208
+ result_owner_id: batch.resultOwnerId,
2209
+ phase: observation.phase,
2210
+ scenario_name: observation.scenarioName,
2211
+ simulation_kind: observation.simulationKind,
2212
+ is_final_attempt: observation.isFinalAttempt ? "true" : "false"
2213
+ };
2214
+ events.push({
2215
+ runId: batch.runId,
2216
+ eventType: "iteration.observation",
2217
+ occurredUtc: new Date(),
2218
+ sessionId: batch.sessionId,
2219
+ testSuite: session.testSuite,
2220
+ testName: session.testName,
2221
+ clusterId: session.clusterId,
2222
+ nodeType: session.nodeType,
2223
+ machineName: session.machineName,
2224
+ scenarioName: observation.scenarioName,
2225
+ stepName: null,
2226
+ tags,
2227
+ fields: {
2228
+ schema_version: observation.schemaVersion,
2229
+ batch_id: batch.batchId,
2230
+ observation_id: observation.observationId,
2231
+ iteration_id: observation.iterationId,
2232
+ process_group: observation.processGroup,
2233
+ scenario_index: observation.scenarioIndex,
2234
+ simulation_index: observation.simulationIndex,
2235
+ global_ordinal_64: observation.globalOrdinal64,
2236
+ shard_index: observation.shardIndex,
2237
+ shard_count: observation.shardCount,
2238
+ attempt_index: observation.attemptIndex,
2239
+ started_utc_ns: observation.startedUtcNs,
2240
+ completed_utc_ns: observation.completedUtcNs,
2241
+ observed_latency_us_64: observation.observedLatencyUs64,
2242
+ reported_latency_us_64: observation.reportedLatencyUs64,
2243
+ is_success: observation.isSuccess,
2244
+ status_code: observation.statusCode,
2245
+ size_bytes_64: observation.sizeBytes64
2246
+ }
2247
+ });
2248
+ for (const step of observation.steps) {
2249
+ events.push({
2250
+ runId: batch.runId,
2251
+ eventType: "iteration.step",
2252
+ occurredUtc: new Date(),
2253
+ sessionId: batch.sessionId,
2254
+ testSuite: session.testSuite,
2255
+ testName: session.testName,
2256
+ clusterId: session.clusterId,
2257
+ nodeType: session.nodeType,
2258
+ machineName: session.machineName,
2259
+ scenarioName: observation.scenarioName,
2260
+ stepName: step.stepName,
2261
+ tags: { ...tags },
2262
+ fields: {
2263
+ schema_version: observation.schemaVersion,
2264
+ batch_id: batch.batchId,
2265
+ observation_id: observation.observationId,
2266
+ iteration_id: observation.iterationId,
2267
+ attempt_index: observation.attemptIndex,
2268
+ sort_index: step.sortIndex,
2269
+ started_utc_ns: step.startedUtcNs,
2270
+ completed_utc_ns: step.completedUtcNs,
2271
+ observed_latency_us_64: step.observedLatencyUs64,
2272
+ reported_latency_us_64: step.reportedLatencyUs64,
2273
+ is_success: step.isSuccess,
2274
+ status_code: step.statusCode,
2275
+ size_bytes_64: step.sizeBytes64
2276
+ }
2277
+ });
2278
+ }
2279
+ }
2280
+ return events;
2281
+ }
2282
+ function createIterationCompletionEvents(session, completion) {
2283
+ return [{
2284
+ runId: completion.runId,
2285
+ eventType: "observation.stream.completed",
2286
+ occurredUtc: new Date(),
2287
+ sessionId: completion.sessionId,
2288
+ testSuite: session.testSuite,
2289
+ testName: session.testName,
2290
+ clusterId: session.clusterId,
2291
+ nodeType: session.nodeType,
2292
+ machineName: session.machineName,
2293
+ scenarioName: null,
2294
+ stepName: null,
2295
+ tags: {
2296
+ run_id: completion.runId,
2297
+ result_owner_id: completion.resultOwnerId,
2298
+ phase: "final"
2299
+ },
2300
+ fields: {
2301
+ schema_version: completion.schemaVersion,
2302
+ process_group: completion.processGroup,
2303
+ last_batch_sequence_64: completion.lastBatchSequence64,
2304
+ captured_count_64: completion.capturedCount64,
2305
+ delivered_count_64: completion.deliveredCount64,
2306
+ dropped_buffer_count_64: completion.droppedBufferCount64,
2307
+ dropped_sink_count_64: completion.droppedSinkCount64,
2308
+ reporting_complete: completion.reportingComplete,
2309
+ completed_utc_ns: completion.completedUtcNs
2310
+ }
2311
+ }];
2312
+ }
1854
2313
  function portalEventPayload(event, index) {
1855
2314
  return {
1856
2315
  eventId: portalEventId(event, index),
@@ -1923,10 +2382,6 @@ function addMeasurementFields(fields, prefix, measurement) {
1923
2382
  fields[`${prefix}_latency_min_ms`] = latency.minMs ?? 0;
1924
2383
  fields[`${prefix}_latency_mean_ms`] = latency.meanMs ?? 0;
1925
2384
  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
2385
  fields[`${prefix}_latency_std_dev`] = latency.stdDev ?? 0;
1931
2386
  fields[`${prefix}_latency_le_800_count`] = latencyCount.lessOrEq800 ?? 0;
1932
2387
  fields[`${prefix}_latency_gt_800_lt_1200_count`] = latencyCount.more800Less1200 ?? 0;
@@ -1935,10 +2390,6 @@ function addMeasurementFields(fields, prefix, measurement) {
1935
2390
  fields[`${prefix}_bytes_min`] = dataTransfer.minBytes ?? 0;
1936
2391
  fields[`${prefix}_bytes_mean`] = dataTransfer.meanBytes ?? 0;
1937
2392
  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
2393
  fields[`${prefix}_bytes_std_dev`] = dataTransfer.stdDev ?? 0;
1943
2394
  fields[`${prefix}_status_code_count`] = statusCodes.length;
1944
2395
  }
@@ -3074,6 +3525,9 @@ function deepCloneValue(value) {
3074
3525
  }
3075
3526
  return value;
3076
3527
  }
3528
+ function gzipJsonRequestBody(value) {
3529
+ return Uint8Array.from(serializeIterationObservationBatchGzipJson(value)).buffer;
3530
+ }
3077
3531
  async function postWithTimeout(fetchImpl, url, init, timeoutMs, sinkName) {
3078
3532
  const controller = new AbortController();
3079
3533
  const timer = setTimeout(() => controller.abort(), Math.max(timeoutMs, 1));