@loadstrike/loadstrike-sdk 1.0.31001 → 1.0.32601

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.
Files changed (45) hide show
  1. package/README.md +16 -2
  2. package/dist/cjs/internal/prometheus-remote-write.js +37 -0
  3. package/dist/cjs/internal/reporting-sink-http-error.js +17 -0
  4. package/dist/cjs/internal/vendor-metric-payloads.js +390 -0
  5. package/dist/cjs/iteration-observations.js +24 -8
  6. package/dist/cjs/local-report-input.js +21 -0
  7. package/dist/cjs/local.js +48 -63
  8. package/dist/cjs/report-history.js +421 -0
  9. package/dist/cjs/reporting-containment.js +242 -0
  10. package/dist/cjs/reporting-svg.js +116 -0
  11. package/dist/cjs/reporting.js +413 -136
  12. package/dist/cjs/runtime.js +237 -8
  13. package/dist/cjs/sinks.js +1337 -38
  14. package/dist/cjs/transports.js +1339 -151
  15. package/dist/esm/internal/prometheus-remote-write.js +31 -0
  16. package/dist/esm/internal/reporting-sink-http-error.js +13 -0
  17. package/dist/esm/internal/vendor-metric-payloads.js +382 -0
  18. package/dist/esm/iteration-observations.js +24 -8
  19. package/dist/esm/local-report-input.js +17 -0
  20. package/dist/esm/local.js +49 -64
  21. package/dist/esm/report-history.js +413 -0
  22. package/dist/esm/reporting-containment.js +238 -0
  23. package/dist/esm/reporting-svg.js +113 -0
  24. package/dist/esm/reporting.js +413 -136
  25. package/dist/esm/runtime.js +239 -10
  26. package/dist/esm/sinks.js +1334 -35
  27. package/dist/esm/transports.js +1335 -151
  28. package/dist/types/contracts.d.ts +1 -0
  29. package/dist/types/index.d.ts +1 -1
  30. package/dist/types/internal/prometheus-remote-write.d.ts +2 -0
  31. package/dist/types/internal/reporting-sink-http-error.d.ts +6 -0
  32. package/dist/types/internal/vendor-metric-payloads.d.ts +48 -0
  33. package/dist/types/local-report-input.d.ts +6 -0
  34. package/dist/types/local.d.ts +0 -6
  35. package/dist/types/report-history.d.ts +124 -0
  36. package/dist/types/reporting-containment.d.ts +2 -0
  37. package/dist/types/reporting-svg.d.ts +2 -0
  38. package/dist/types/reporting.d.ts +6 -3
  39. package/dist/types/runtime.d.ts +24 -0
  40. package/dist/types/sinks.d.ts +134 -17
  41. package/dist/types/transports.d.ts +2 -0
  42. package/package.json +9 -3
  43. package/dist/cjs/internal-build.js +0 -4
  44. package/dist/esm/internal-build.js +0 -1
  45. package/dist/types/internal-build.d.ts +0 -1
@@ -5,15 +5,20 @@ import { resolve } from "node:path";
5
5
  import { LoadStrikeLocalClient } from "./local.js";
6
6
  import { DistributedClusterAgent, DistributedClusterCoordinator, buildLoadEngineV2ReservedStepOtherIdentityKey, buildLoadEngineV2GlobalInvocationId, buildLoadEngineV2ScenarioIdentityKey, buildLoadEngineV2SchedulerIdentityKey, buildLoadEngineV2StatusIdentityKey, buildLoadEngineV2StepIdentityKey, resolveLoadEngineV2StepIdentityKey, buildLoadEngineV2Plan } from "./cluster.js";
7
7
  import { CorrelationStoreConfiguration, CrossPlatformTrackingRuntime, RedisCorrelationStore, RedisCorrelationStoreOptions, TrackingFieldSelector } from "./correlation.js";
8
- import { EndpointAdapterFactory, LOADSTRIKE_TRACE_ID_TRACKING_FIELD } from "./transports.js";
8
+ import { EndpointAdapterFactory, LOADSTRIKE_TRACE_ID_TRACKING_FIELD, validateNativeEndpointExecutionSupport } from "./transports.js";
9
9
  import { buildDotnetCsvReport, buildDotnetHtmlReport, buildDotnetMarkdownReport, buildDotnetTxtReport } from "./reporting.js";
10
- import { PortalReportingSink, cloneReportingSinkForRun } from "./sinks.js";
10
+ import { ReportHistoryCollector, ReportHistoryLifecycleCoordinator, ReportHistoryWorker, sanitizedExceptionClassChain } from "./report-history.js";
11
+ import { distributedLocalReportInput, emptyLocalReportInput } from "./local-report-input.js";
12
+ import { PortalReportingSink, PrometheusRemoteWriteReportingSink, cloneReportingSinkForRun } from "./sinks.js";
13
+ import { ReportingSinkHttpError } from "./internal/reporting-sink-http-error.js";
14
+ import { assertNoUnsupportedReportingSinkGraph } from "./reporting-containment.js";
11
15
  import { LoadEngineV2ExecutionBudget, buildLoadEngineV2TrafficMixSeedId, LoadStrikeHistogramV1, parseLoadEngineV2HistogramArtifact, serializeLoadEngineV2HistogramArtifact, classifyLoadEngineV2Arrival, loadEngineV2FixedArrivalCount, loadEngineV2FixedDeadlineNs, loadEngineV2LatenessToleranceNs, loadEngineV2TrafficMixLaneUnitCount, loadEngineV2TrafficMixOwnedUnits, planRampingInjectionDeadlines, planRampingConstantDeadlines, planRandomInjectionDeadlines, fnv1a32 } from "./load-engine-v2.js";
12
16
  import { DEFAULT_ITERATION_OBSERVATION_SETTINGS, IterationObservationReporter, createIterationObservation, createIterationStepObservation, utcNowNs, validateIterationObservationSettings } from "./iteration-observations.js";
13
17
  import { logIterationObservationFailure, logIterationObservationRecovery } from "./iteration-observation-diagnostics.js";
14
18
  import { normalizeSinkRetryBackoffMs, normalizeSinkRetryCount, sinkRetryDelayMs, waitForSinkRetryDelay } from "./sink-retry-policy.js";
15
19
  const LOAD_ENGINE_V2_SCHEDULER_DISTRIBUTIONS = Symbol("loadstrike.load-engine-v2.scheduler-distributions");
16
20
  const PORTAL_RUN_TOKEN_PROVIDER = Symbol.for("loadstrike.internal.portal-run-token-provider");
21
+ const REPORTING_INTERVAL_SECONDS = Symbol.for("loadstrike.internal.reporting-interval-seconds");
17
22
  export const LoadStrikeNodeType = {
18
23
  SingleNode: "SingleNode",
19
24
  Coordinator: "Coordinator",
@@ -283,6 +288,28 @@ class MeasurementAccumulator {
283
288
  moreOrEq1200: left.moreOrEq1200 + right.moreOrEq1200
284
289
  }, allRequestCount, durationMs);
285
290
  }
291
+ /**
292
+ * Projects only the cumulative fields used by the private HTML-report
293
+ * history. Legacy distributions deliberately omit latency so this cadence
294
+ * path never scans their retained per-request arrays.
295
+ */
296
+ buildReportHistoryMeasurement() {
297
+ return projectNativeReportHistoryMeasurement(this.count, this.allBytes, this.useHistogram ? this.latencyHistogram : undefined);
298
+ }
299
+ /**
300
+ * Combines only bounded native latency state plus exact counters. It does
301
+ * not materialize status rows or any public measurement DTO.
302
+ */
303
+ buildCombinedReportHistoryMeasurement(other) {
304
+ const count = this.count + other.count;
305
+ const bytes = this.allBytes + other.allBytes;
306
+ if (!this.useHistogram || !other.useHistogram) {
307
+ return projectNativeReportHistoryMeasurement(count, bytes);
308
+ }
309
+ const latency = this.latencyHistogram.clone();
310
+ latency.merge(other.latencyHistogram);
311
+ return projectNativeReportHistoryMeasurement(count, bytes, latency);
312
+ }
286
313
  histogramSnapshot() {
287
314
  return {
288
315
  count: this.count,
@@ -296,6 +323,24 @@ class MeasurementAccumulator {
296
323
  };
297
324
  }
298
325
  }
326
+ function projectNativeReportHistoryMeasurement(count, bytes, latency) {
327
+ const measurement = {
328
+ count,
329
+ bytes,
330
+ approximate: latency?.maxRelativeError !== undefined
331
+ && latency.maxRelativeError > 0
332
+ };
333
+ if (!latency || latency.count === 0n) {
334
+ return measurement;
335
+ }
336
+ return {
337
+ ...measurement,
338
+ percent50Ms: Number(latency.percentile(0.5)) / 1000,
339
+ percent75Ms: Number(latency.percentile(0.75)) / 1000,
340
+ percent95Ms: Number(latency.percentile(0.95)) / 1000,
341
+ percent99Ms: Number(latency.percentile(0.99)) / 1000
342
+ };
343
+ }
299
344
  function buildHistogramMeasurement(snapshot, allRequestCount, durationMs) {
300
345
  const count = snapshot.count;
301
346
  const totalDurationMs = Math.max(durationMs, 0);
@@ -634,6 +679,21 @@ class ScenarioStatsAccumulator {
634
679
  step.record(reply, observedLatencyMs);
635
680
  return step.sortIndex;
636
681
  }
682
+ /**
683
+ * Captures the bounded, private history view without building scenario
684
+ * steps, status rows, plugins, aliases, or any other public result shape.
685
+ */
686
+ buildReportHistorySnapshot() {
687
+ const ok = this.ok.buildReportHistoryMeasurement();
688
+ const failed = this.fail.buildReportHistoryMeasurement();
689
+ return {
690
+ scenarioName: this.scenarioName,
691
+ sortIndex: this.sortIndex,
692
+ all: this.ok.buildCombinedReportHistoryMeasurement(this.fail),
693
+ ...(ok.count > 0 ? { ok } : {}),
694
+ ...(failed.count > 0 ? { failed } : {})
695
+ };
696
+ }
637
697
  /**
638
698
  * Builds the configured payload or helper object.
639
699
  * Use this when all builder inputs are ready to be materialized.
@@ -682,6 +742,15 @@ class ScenarioStatsAccumulator {
682
742
  return attachScenarioStatsAliases(scenario);
683
743
  }
684
744
  }
745
+ function isRetryableReportingSinkError(sink, error) {
746
+ if (!(sink instanceof PrometheusRemoteWriteReportingSink)) {
747
+ return true;
748
+ }
749
+ if (!(error instanceof ReportingSinkHttpError)) {
750
+ return true;
751
+ }
752
+ return error.status === 429 || error.status < 400 || error.status >= 500;
753
+ }
685
754
  export class LoadStrikeResponse {
686
755
  /**
687
756
  * Creates a successful reply.
@@ -3410,6 +3479,8 @@ export class LoadStrikeRunner {
3410
3479
  return this.buildContext();
3411
3480
  }
3412
3481
  async run(args = []) {
3482
+ assertNoUnsupportedReportingSinkGraph(this.options.reportingSinks ?? []);
3483
+ assertNoUnsupportedNativeTrackingScenarios(this.scenarios);
3413
3484
  if (this.contextConfigurators.length) {
3414
3485
  return new LoadStrikeRunner(this.scenarios, this.buildContext().toRunnerOptions(), [], this.internalOptions).run(args);
3415
3486
  }
@@ -3421,7 +3492,7 @@ export class LoadStrikeRunner {
3421
3492
  const scenarioAccumulators = new Map();
3422
3493
  const scenarioDurationsMs = new Map();
3423
3494
  const stepStats = new Map();
3424
- const sinks = (this.options.reportingSinks ?? []).map((sink) => cloneReportingSinkForRun(sink));
3495
+ const sinks = (this.options.reportingSinks ?? []).map((sink) => cloneReportingSinkForRun(sink, this.options.infraConfig ?? {}));
3425
3496
  const sinkStates = sinks.map((sink, index) => ({
3426
3497
  sink,
3427
3498
  disabled: false,
@@ -3481,6 +3552,58 @@ export class LoadStrikeRunner {
3481
3552
  licenseSession = await licenseClient.acquireLicenseLease(licensePayload);
3482
3553
  const loggerSetup = createLoggerSetup(this.options.loggerConfig, this.options.minimumLogLevel, this.options, testInfo, nodeInfo);
3483
3554
  const runLogger = loggerSetup.logger;
3555
+ const htmlReportHistoryRequested = (this.options.reportsEnabled ?? true)
3556
+ && normalizeReportFormats(this.options.reportFormats ?? ["html", "txt", "csv", "md"]).includes("html");
3557
+ const distributedReportHistory = isDistributedReportHistoryExecution(clusterMode, this.options);
3558
+ let localReportInput = htmlReportHistoryRequested
3559
+ && distributedReportHistory
3560
+ ? distributedLocalReportInput()
3561
+ : emptyLocalReportInput();
3562
+ let reportHistoryCollector;
3563
+ let reportHistoryLifecycle;
3564
+ if (htmlReportHistoryRequested && !distributedReportHistory) {
3565
+ const warnReportHistoryFailure = (category, error) => {
3566
+ const exceptionClasses = error === undefined
3567
+ ? ""
3568
+ : `; exception_classes=${sanitizedExceptionClassChain(error)}`;
3569
+ try {
3570
+ runLogger.warn(`LoadStrike report history unavailable: ${category}${exceptionClasses}.`);
3571
+ }
3572
+ catch {
3573
+ // Report-only diagnostics are best-effort.
3574
+ }
3575
+ };
3576
+ reportHistoryCollector = new ReportHistoryCollector({
3577
+ scenarioCount: selectedScenarios.length,
3578
+ onCaptureError: (error) => warnReportHistoryFailure("capture_failure", error)
3579
+ });
3580
+ if (!reportHistoryCollector.available) {
3581
+ warnReportHistoryFailure("budget_pressure");
3582
+ }
3583
+ const snapshotFactory = () => Array.from(scenarioAccumulators.values())
3584
+ .map((value) => value.buildReportHistorySnapshot())
3585
+ .sort((left, right) => (left.sortIndex ?? 0) - (right.sortIndex ?? 0)
3586
+ || left.scenarioName.localeCompare(right.scenarioName));
3587
+ const reportHistoryWorker = new ReportHistoryWorker({
3588
+ collector: reportHistoryCollector,
3589
+ cadenceSeconds: resolvedReportHistoryCadenceSeconds(this.options.reportingIntervalSeconds ?? 5),
3590
+ snapshotFactory,
3591
+ onFailure: (category, exceptionClasses) => {
3592
+ try {
3593
+ runLogger.warn(`LoadStrike report history unavailable: ${category}; exception_classes=${exceptionClasses}.`);
3594
+ }
3595
+ catch {
3596
+ // Report-only diagnostics are best-effort.
3597
+ }
3598
+ }
3599
+ });
3600
+ reportHistoryLifecycle = new ReportHistoryLifecycleCoordinator({
3601
+ scenarioCount: selectedScenarios.length,
3602
+ start: () => reportHistoryWorker.start(),
3603
+ stopAndFinalize: () => reportHistoryWorker.stopAndFinalize(),
3604
+ stopWithoutFinalizing: () => reportHistoryWorker.stopWithoutFinalizing()
3605
+ });
3606
+ }
3484
3607
  const scenarioStartInfos = selectedScenarios.map((scenario, index) => {
3485
3608
  const startInfo = {
3486
3609
  scenarioName: scenario.name,
@@ -3494,6 +3617,12 @@ export class LoadStrikeRunner {
3494
3617
  testInfo,
3495
3618
  getNodeInfo: () => attachNodeInfoAliases({ ...nodeInfo })
3496
3619
  };
3620
+ Object.defineProperty(baseContext, REPORTING_INTERVAL_SECONDS, {
3621
+ value: this.options.reportingIntervalSeconds ?? 5,
3622
+ enumerable: false,
3623
+ configurable: false,
3624
+ writable: false
3625
+ });
3497
3626
  attachBaseContextAliases(baseContext);
3498
3627
  const sinkSession = {
3499
3628
  startedUtc: createdUtc,
@@ -3548,6 +3677,7 @@ export class LoadStrikeRunner {
3548
3677
  name: state.name,
3549
3678
  iterationObservationPortalSink: Boolean(state.sink.iterationObservationPortalSink),
3550
3679
  iterationObservationShapeLimited: Boolean(state.sink.iterationObservationShapeLimited),
3680
+ retryableErrorClassifier: (error) => isRetryableReportingSinkError(state.sink, error),
3551
3681
  saveIterationBatch: resolveSinkSaveIterationBatch(state.sink),
3552
3682
  completeIterationObservationStream: resolveSinkCompleteIterationObservationStream(state.sink)
3553
3683
  }));
@@ -3590,7 +3720,7 @@ export class LoadStrikeRunner {
3590
3720
  realtimeInFlight = false;
3591
3721
  }
3592
3722
  };
3593
- const reportingIntervalMs = Math.max(Math.trunc((this.options.reportingIntervalSeconds ?? 5) * 1000), 1);
3723
+ const reportingIntervalMs = boundedReportingIntervalMs(this.options.reportingIntervalSeconds ?? 5);
3594
3724
  const triggerRealtimeSnapshot = () => {
3595
3725
  if (realtimeCurrent) {
3596
3726
  return;
@@ -3677,6 +3807,7 @@ export class LoadStrikeRunner {
3677
3807
  iterationObservationRunId,
3678
3808
  iterationObservationResultOwnerId,
3679
3809
  iterationObservationProcessGroup,
3810
+ reportHistoryLifecycle,
3680
3811
  executeScenarioInvocation: (targetScenario, context, operation) => this.executeScenarioInvocation(targetScenario, context, operation),
3681
3812
  invokeBeforeScenario: (runtimePolicies, scenarioName) => this.invokeBeforeScenario(runtimePolicies, scenarioName, policyErrors, runtimePolicyErrorMode),
3682
3813
  invokeAfterScenario: (runtimePolicies, scenarioName, stats) => this.invokeAfterScenario(runtimePolicies, scenarioName, stats, policyErrors, runtimePolicyErrorMode),
@@ -3764,9 +3895,14 @@ export class LoadStrikeRunner {
3764
3895
  ...iterationObservationReporter.buildWarnings()
3765
3896
  ];
3766
3897
  result.pluginsData = mergePluginData(result.pluginsData, await this.collectPluginData(plugins, attachRunResultAliases(result), pluginLifecycleErrors));
3898
+ if (reportHistoryCollector) {
3899
+ localReportInput = {
3900
+ history: reportHistoryCollector.toProjection()
3901
+ };
3902
+ }
3767
3903
  const finalizedResult = attachRunResultAliases(result);
3768
3904
  finalizedResult.logFiles = mergeStringArrays(finalizedResult.logFiles, loggerSetup.logFiles);
3769
- finalizedResult.reportFiles = this.writeReports(finalizedResult);
3905
+ finalizedResult.reportFiles = this.writeReports(finalizedResult, localReportInput);
3770
3906
  finalizedResult.disabledSinks = sinkStates.filter((x) => x.disabled).map((x) => x.name);
3771
3907
  await this.emitRunResult(sinkStates, finalizedResult, sinkRetryCount, sinkRetryBackoffMs, sinkErrors, testInfo.sessionId, runLogger);
3772
3908
  await this.stopSinks(sinkStates, sinkRetryCount, sinkRetryBackoffMs, sinkErrors, testInfo.sessionId, runLogger);
@@ -3777,6 +3913,7 @@ export class LoadStrikeRunner {
3777
3913
  return finalizedResult;
3778
3914
  }
3779
3915
  finally {
3916
+ reportHistoryLifecycle?.stopWithoutFinalizing();
3780
3917
  await stopRealtimeReporting();
3781
3918
  if (!iterationObservationsFinalized) {
3782
3919
  await iterationObservationReporter.sealAndDrain().catch(() => { });
@@ -4175,7 +4312,9 @@ export class LoadStrikeRunner {
4175
4312
  return;
4176
4313
  }
4177
4314
  catch (error) {
4178
- const exhausted = attempts >= maximumAttempts;
4315
+ const retryable = isRetryableReportingSinkError(state.sink, error);
4316
+ const exhausted = attempts >= maximumAttempts || !retryable;
4317
+ const reportedMaximumAttempts = retryable ? maximumAttempts : attempts;
4179
4318
  const nextDelayMs = exhausted ? 0 : sinkRetryDelayMs(backoffMs, attempts);
4180
4319
  logIterationObservationFailure(logger, exhausted ? "error" : "warn", {
4181
4320
  sinkName: state.name,
@@ -4185,7 +4324,7 @@ export class LoadStrikeRunner {
4185
4324
  resultOwnerId: "",
4186
4325
  observationCount: 0,
4187
4326
  attempt: attempts,
4188
- maximumAttempts,
4327
+ maximumAttempts: reportedMaximumAttempts,
4189
4328
  nextDelayMs
4190
4329
  }, error);
4191
4330
  if (exhausted) {
@@ -4254,7 +4393,7 @@ export class LoadStrikeRunner {
4254
4393
  message: String(error ?? "runtime policy callback failed")
4255
4394
  });
4256
4395
  }
4257
- writeReports(result) {
4396
+ writeReports(result, localReportInput = emptyLocalReportInput()) {
4258
4397
  const reportsEnabled = this.options.reportsEnabled ?? true;
4259
4398
  if (!reportsEnabled) {
4260
4399
  return [];
@@ -4279,7 +4418,7 @@ export class LoadStrikeRunner {
4279
4418
  writeFileSync(path, buildDotnetMarkdownReport(nodeStats), "utf8");
4280
4419
  }
4281
4420
  else if (format === "html") {
4282
- writeFileSync(path, buildDotnetHtmlReport(nodeStats), "utf8");
4421
+ writeFileSync(path, buildDotnetHtmlReport(nodeStats, localReportInput), "utf8");
4283
4422
  }
4284
4423
  written.push(path);
4285
4424
  }
@@ -4312,6 +4451,19 @@ function hasPluginRows(value) {
4312
4451
  }
4313
4452
  return value.tables.some((table) => Array.isArray(table.rows) && table.rows.length > 0);
4314
4453
  }
4454
+ function boundedReportingIntervalMs(seconds) {
4455
+ if (!Number.isFinite(seconds) || seconds <= 0) {
4456
+ return 1;
4457
+ }
4458
+ const milliseconds = seconds * 1000;
4459
+ if (!Number.isFinite(milliseconds) || milliseconds >= 2147483647) {
4460
+ return 2147483647;
4461
+ }
4462
+ return Math.max(Math.trunc(milliseconds), 1);
4463
+ }
4464
+ function resolvedReportHistoryCadenceSeconds(seconds) {
4465
+ return boundedReportingIntervalMs(seconds) / 1000;
4466
+ }
4315
4467
  async function executeV2FixedArrivals(args) {
4316
4468
  const { rate, intervalNs, totalArrivals, budget, cancellationToken, shouldStopNow, nextInstanceInfo, runBombingInvocation, logger, scenarioName, deadlineOffsetsNs, tolerancesNs, ownedOrdinals, shardIndex = 0, shardCount = 1, telemetry, segment } = args;
4317
4469
  const segmentStartNs = process.hrtime.bigint();
@@ -4448,7 +4600,15 @@ function maxBigInt(left, right) {
4448
4600
  return left > right ? left : right;
4449
4601
  }
4450
4602
  async function executeScenarioRuntime(args) {
4451
- const { scenario, scenarioIndex, scenarioCount, options, logger, nodeInfo, testInfo, policies, restartIterationMaxAttempts, allRegisteredMetrics, scenarioRuntimes, stepRuntimes, scenarioAccumulators, scenarioDurationsMs, stopTestState, testAbortController, loadEngineV2Budget, loadEngineV2Telemetry, iterationObservationReporter, iterationObservationRunId = testInfo.sessionId, iterationObservationResultOwnerId = "", iterationObservationProcessGroup = 0, executeScenarioInvocation, invokeBeforeScenario, invokeAfterScenario, invokeBeforeStep, invokeAfterStep } = args;
4603
+ const { scenario, scenarioIndex, scenarioCount, options, logger, nodeInfo, testInfo, policies, restartIterationMaxAttempts, allRegisteredMetrics, scenarioRuntimes, stepRuntimes, scenarioAccumulators, scenarioDurationsMs, stopTestState, testAbortController, loadEngineV2Budget, loadEngineV2Telemetry, iterationObservationReporter, iterationObservationRunId = testInfo.sessionId, iterationObservationResultOwnerId = "", iterationObservationProcessGroup = 0, reportHistoryLifecycle, executeScenarioInvocation, invokeBeforeScenario, invokeAfterScenario, invokeBeforeStep, invokeAfterStep } = args;
4604
+ let reportHistoryExecutionEnded = false;
4605
+ const endReportHistoryExecution = async () => {
4606
+ if (reportHistoryExecutionEnded) {
4607
+ return;
4608
+ }
4609
+ reportHistoryExecutionEnded = true;
4610
+ await reportHistoryLifecycle?.scenarioExecutionEnded();
4611
+ };
4452
4612
  const scenarioStartedMs = Date.now();
4453
4613
  const scenarioContextData = {};
4454
4614
  const registeredMetrics = [];
@@ -5269,6 +5429,7 @@ async function executeScenarioRuntime(args) {
5269
5429
  })));
5270
5430
  await invokeBeforeScenario(policies, scenario.name);
5271
5431
  await runWarmUpAsync();
5432
+ reportHistoryLifecycle?.scenarioBombingStarted();
5272
5433
  accumulator.setCurrentOperation("Bombing");
5273
5434
  const simulations = scenario.getSimulations();
5274
5435
  if (!simulations.length) {
@@ -5302,6 +5463,7 @@ async function executeScenarioRuntime(args) {
5302
5463
  }
5303
5464
  }
5304
5465
  accumulator.setCurrentOperation(stopScenario || stopTestState.value ? "Stop" : "Complete");
5466
+ await endReportHistoryExecution();
5305
5467
  await invokeAfterScenario(policies, scenario.name, runtime);
5306
5468
  }
5307
5469
  catch (error) {
@@ -5318,6 +5480,7 @@ async function executeScenarioRuntime(args) {
5318
5480
  }
5319
5481
  }
5320
5482
  finally {
5483
+ await endReportHistoryExecution();
5321
5484
  scenarioDurationsMs.set(scenario.name, Math.max(Date.now() - scenarioStartedMs, 0));
5322
5485
  try {
5323
5486
  await scenario.invokeClean({
@@ -7251,6 +7414,11 @@ function resolveClusterExecutionMode(options) {
7251
7414
  }
7252
7415
  return "single";
7253
7416
  }
7417
+ function isDistributedReportHistoryExecution(clusterMode, options) {
7418
+ return clusterMode !== "single"
7419
+ || options.loadEngineV2SegmentLifecycleOverride !== undefined
7420
+ || Math.max(Math.trunc(options.clusterShardCount ?? 1), 1) > 1;
7421
+ }
7254
7422
  function buildEmptyNodeStats(args) {
7255
7423
  return attachNodeStatsAliases({
7256
7424
  startedUtc: args.startedUtc,
@@ -8722,6 +8890,7 @@ class ManagedScenarioTrackingRuntime {
8722
8890
  }
8723
8891
  async dispose() {
8724
8892
  this.shutdown = true;
8893
+ this.interruptEndpointAdapters();
8725
8894
  this.rejectOutstandingWaiters();
8726
8895
  await Promise.all([
8727
8896
  this.sourceLoop,
@@ -8787,6 +8956,7 @@ class ManagedScenarioTrackingRuntime {
8787
8956
  failureSeen = failureSeen || this.isFailedObservationOutcome(outcome);
8788
8957
  }
8789
8958
  this.shutdown = true;
8959
+ this.interruptEndpointAdapters();
8790
8960
  await Promise.all([
8791
8961
  this.sourceLoop,
8792
8962
  this.destinationLoop,
@@ -8803,6 +8973,20 @@ class ManagedScenarioTrackingRuntime {
8803
8973
  ? LoadStrikeResponse.fail("tracking_failures", "One or more observed source or destination events did not correlate successfully.", 0)
8804
8974
  : LoadStrikeResponse.ok("observed");
8805
8975
  }
8976
+ interruptEndpointAdapters() {
8977
+ try {
8978
+ this.sourceAdapter.interrupt?.();
8979
+ }
8980
+ catch {
8981
+ // Shutdown remains best-effort and still disposes every adapter below.
8982
+ }
8983
+ try {
8984
+ this.destinationAdapter?.interrupt?.();
8985
+ }
8986
+ catch {
8987
+ // Shutdown remains best-effort and still disposes every adapter below.
8988
+ }
8989
+ }
8806
8990
  async consumeSourceLoop() {
8807
8991
  while (!this.shutdown) {
8808
8992
  try {
@@ -9394,6 +9578,20 @@ function mapRuntimeTrackingEndpointSpec(spec, useLoadStrikeTraceIdHeader = false
9394
9578
  azureEventHubs: asTrackingRecord(pickTrackingValue(spec, "AzureEventHubs", "azureEventHubs")),
9395
9579
  sqs: asTrackingRecord(pickTrackingValue(spec, "Sqs", "sqs")),
9396
9580
  pushDiffusion: asTrackingRecord(pickTrackingValue(spec, "PushDiffusion", "pushDiffusion")),
9581
+ grpc: mapRuntimeEndpointProtocolOptions(spec, ["Grpc", "grpc"], [
9582
+ "Target", "target", "ServiceName", "serviceName", "MethodName", "methodName",
9583
+ "MethodType", "methodType", "Deadline", "deadline", "DeadlineSeconds", "deadlineSeconds",
9584
+ "DeadlineMs", "deadlineMs", "Metadata", "metadata", "Produce", "produce", "Consume", "consume",
9585
+ "ProduceAsync", "produceAsync", "ConsumeAsync", "consumeAsync", "ConnectionMetadata", "connectionMetadata",
9586
+ "NativeClient", "nativeClient"
9587
+ ]),
9588
+ webSocket: mapRuntimeEndpointProtocolOptions(spec, ["WebSocket", "webSocket"], [
9589
+ "Url", "url", "Subprotocols", "subprotocols", "ConnectTimeout", "connectTimeout",
9590
+ "ConnectTimeoutSeconds", "connectTimeoutSeconds", "ConnectTimeoutMs", "connectTimeoutMs",
9591
+ "CloseTimeout", "closeTimeout", "CloseTimeoutSeconds", "closeTimeoutSeconds", "CloseTimeoutMs", "closeTimeoutMs",
9592
+ "Produce", "produce", "Consume", "consume", "ProduceAsync", "produceAsync", "ConsumeAsync", "consumeAsync",
9593
+ "ConnectionMetadata", "connectionMetadata", "NativeClient", "nativeClient"
9594
+ ]),
9397
9595
  delegate: typeof delegateProduce === "function"
9398
9596
  || typeof delegateConsume === "function"
9399
9597
  || typeof delegateProduceAsync === "function"
@@ -9416,6 +9614,19 @@ function mapRuntimeTrackingEndpointSpec(spec, useLoadStrikeTraceIdHeader = false
9416
9614
  : undefined
9417
9615
  };
9418
9616
  }
9617
+ function mapRuntimeEndpointProtocolOptions(spec, nestedKeys, flatKeys) {
9618
+ const nested = asTrackingRecord(pickTrackingValue(spec, ...nestedKeys));
9619
+ if (Object.keys(nested).length > 0) {
9620
+ return nested;
9621
+ }
9622
+ const options = {};
9623
+ for (const key of flatKeys) {
9624
+ if (Object.prototype.hasOwnProperty.call(spec, key)) {
9625
+ options[key] = pickTrackingValue(spec, key);
9626
+ }
9627
+ }
9628
+ return options;
9629
+ }
9419
9630
  function normalizeRuntimeTrackingPayload(payload, endpoint, index) {
9420
9631
  const normalized = {
9421
9632
  headers: {
@@ -10179,6 +10390,20 @@ function assertNoDisableLicenseEnforcementOption(value, source) {
10179
10390
  }
10180
10391
  }
10181
10392
  }
10393
+ function assertNoUnsupportedNativeTrackingScenarios(scenarios) {
10394
+ for (const scenario of scenarios) {
10395
+ const tracking = scenario.getTrackingConfiguration();
10396
+ if (!tracking) {
10397
+ continue;
10398
+ }
10399
+ for (const field of ["Source", "Destination"]) {
10400
+ const endpoint = asTrackingRecord(pickTrackingValue(tracking, field, field.toLowerCase()));
10401
+ if (Object.keys(endpoint).length > 0) {
10402
+ validateNativeEndpointExecutionSupport(endpoint);
10403
+ }
10404
+ }
10405
+ }
10406
+ }
10182
10407
  function normalizeRunContextCollectionShapes(values) {
10183
10408
  assertNoDisableLicenseEnforcementOption(values, "LoadStrikeContext");
10184
10409
  const normalized = {
@@ -11082,6 +11307,7 @@ function buildGroupedCorrelationRows(rows) {
11082
11307
  LatencyMinMs: latencySamples.length ? formatOptionalLatency(latencySamples[0]) : "",
11083
11308
  LatencyMeanMs: latencySamples.length ? formatOptionalLatency(latencySamples.reduce((sum, value) => sum + value, 0) / latencySamples.length) : "",
11084
11309
  LatencyP50Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.5)) : "",
11310
+ LatencyP75Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.75)) : "",
11085
11311
  LatencyP80Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.8)) : "",
11086
11312
  LatencyP85Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.85)) : "",
11087
11313
  LatencyP90Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.9)) : "",
@@ -11294,6 +11520,8 @@ export const __loadstrikeTestExports = {
11294
11520
  buildThresholdCheckExpression,
11295
11521
  buildTrackingLeaseKey,
11296
11522
  buildTrackingRunNamespace,
11523
+ boundedReportingIntervalMs,
11524
+ resolvedReportHistoryCadenceSeconds,
11297
11525
  clusterNodeResultToNodeStats,
11298
11526
  combineAbortSignals,
11299
11527
  computeScenarioRequestCount,
@@ -11313,6 +11541,7 @@ export const __loadstrikeTestExports = {
11313
11541
  formatUtcReportTimestamp,
11314
11542
  hasPluginRows,
11315
11543
  inferRuntimeLegacyHttpResponseSource,
11544
+ isDistributedReportHistoryExecution,
11316
11545
  isComparisonFailed,
11317
11546
  loadJsonObject,
11318
11547
  logLevelOrder,