@loadstrike/loadstrike-sdk 1.0.30401 → 1.0.31601

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.
@@ -13,9 +13,13 @@ const cluster_js_1 = require("./cluster.js");
13
13
  const correlation_js_1 = require("./correlation.js");
14
14
  const transports_js_1 = require("./transports.js");
15
15
  const reporting_js_1 = require("./reporting.js");
16
+ const report_history_js_1 = require("./report-history.js");
17
+ const local_report_input_js_1 = require("./local-report-input.js");
16
18
  const sinks_js_1 = require("./sinks.js");
17
19
  const load_engine_v2_js_1 = require("./load-engine-v2.js");
18
20
  const iteration_observations_js_1 = require("./iteration-observations.js");
21
+ const iteration_observation_diagnostics_js_1 = require("./iteration-observation-diagnostics.js");
22
+ const sink_retry_policy_js_1 = require("./sink-retry-policy.js");
19
23
  const LOAD_ENGINE_V2_SCHEDULER_DISTRIBUTIONS = Symbol("loadstrike.load-engine-v2.scheduler-distributions");
20
24
  const PORTAL_RUN_TOKEN_PROVIDER = Symbol.for("loadstrike.internal.portal-run-token-provider");
21
25
  exports.LoadStrikeNodeType = {
@@ -289,6 +293,28 @@ class MeasurementAccumulator {
289
293
  moreOrEq1200: left.moreOrEq1200 + right.moreOrEq1200
290
294
  }, allRequestCount, durationMs);
291
295
  }
296
+ /**
297
+ * Projects only the cumulative fields used by the private HTML-report
298
+ * history. Legacy distributions deliberately omit latency so this cadence
299
+ * path never scans their retained per-request arrays.
300
+ */
301
+ buildReportHistoryMeasurement() {
302
+ return projectNativeReportHistoryMeasurement(this.count, this.allBytes, this.useHistogram ? this.latencyHistogram : undefined);
303
+ }
304
+ /**
305
+ * Combines only bounded native latency state plus exact counters. It does
306
+ * not materialize status rows or any public measurement DTO.
307
+ */
308
+ buildCombinedReportHistoryMeasurement(other) {
309
+ const count = this.count + other.count;
310
+ const bytes = this.allBytes + other.allBytes;
311
+ if (!this.useHistogram || !other.useHistogram) {
312
+ return projectNativeReportHistoryMeasurement(count, bytes);
313
+ }
314
+ const latency = this.latencyHistogram.clone();
315
+ latency.merge(other.latencyHistogram);
316
+ return projectNativeReportHistoryMeasurement(count, bytes, latency);
317
+ }
292
318
  histogramSnapshot() {
293
319
  return {
294
320
  count: this.count,
@@ -302,6 +328,24 @@ class MeasurementAccumulator {
302
328
  };
303
329
  }
304
330
  }
331
+ function projectNativeReportHistoryMeasurement(count, bytes, latency) {
332
+ const measurement = {
333
+ count,
334
+ bytes,
335
+ approximate: latency?.maxRelativeError !== undefined
336
+ && latency.maxRelativeError > 0
337
+ };
338
+ if (!latency || latency.count === 0n) {
339
+ return measurement;
340
+ }
341
+ return {
342
+ ...measurement,
343
+ percent50Ms: Number(latency.percentile(0.5)) / 1000,
344
+ percent75Ms: Number(latency.percentile(0.75)) / 1000,
345
+ percent95Ms: Number(latency.percentile(0.95)) / 1000,
346
+ percent99Ms: Number(latency.percentile(0.99)) / 1000
347
+ };
348
+ }
305
349
  function buildHistogramMeasurement(snapshot, allRequestCount, durationMs) {
306
350
  const count = snapshot.count;
307
351
  const totalDurationMs = Math.max(durationMs, 0);
@@ -640,6 +684,21 @@ class ScenarioStatsAccumulator {
640
684
  step.record(reply, observedLatencyMs);
641
685
  return step.sortIndex;
642
686
  }
687
+ /**
688
+ * Captures the bounded, private history view without building scenario
689
+ * steps, status rows, plugins, aliases, or any other public result shape.
690
+ */
691
+ buildReportHistorySnapshot() {
692
+ const ok = this.ok.buildReportHistoryMeasurement();
693
+ const failed = this.fail.buildReportHistoryMeasurement();
694
+ return {
695
+ scenarioName: this.scenarioName,
696
+ sortIndex: this.sortIndex,
697
+ all: this.ok.buildCombinedReportHistoryMeasurement(this.fail),
698
+ ...(ok.count > 0 ? { ok } : {}),
699
+ ...(failed.count > 0 ? { failed } : {})
700
+ };
701
+ }
643
702
  /**
644
703
  * Builds the configured payload or helper object.
645
704
  * Use this when all builder inputs are ready to be materialized.
@@ -3455,8 +3514,8 @@ class LoadStrikeRunner {
3455
3514
  }));
3456
3515
  const sinkErrors = [];
3457
3516
  const policyErrors = [];
3458
- const sinkRetryCount = Math.max(this.options.sinkRetryCount ?? 2, 0);
3459
- const sinkRetryBackoffMs = Math.max(this.options.sinkRetryBackoffMs ?? 25, 0);
3517
+ const sinkRetryCount = (0, sink_retry_policy_js_1.normalizeSinkRetryCount)(this.options.sinkRetryCount);
3518
+ const sinkRetryBackoffMs = (0, sink_retry_policy_js_1.normalizeSinkRetryBackoffMs)(this.options.sinkRetryBackoffMs);
3460
3519
  const policies = this.options.runtimePolicies ?? [];
3461
3520
  const runtimePolicyErrorMode = normalizedRuntimePolicyErrorMode(this.options.runtimePolicyErrorMode);
3462
3521
  const plugins = this.options.reportingSinks === undefined && this.options.workerPlugins === undefined &&
@@ -3507,6 +3566,58 @@ class LoadStrikeRunner {
3507
3566
  licenseSession = await licenseClient.acquireLicenseLease(licensePayload);
3508
3567
  const loggerSetup = createLoggerSetup(this.options.loggerConfig, this.options.minimumLogLevel, this.options, testInfo, nodeInfo);
3509
3568
  const runLogger = loggerSetup.logger;
3569
+ const htmlReportHistoryRequested = (this.options.reportsEnabled ?? true)
3570
+ && normalizeReportFormats(this.options.reportFormats ?? ["html", "txt", "csv", "md"]).includes("html");
3571
+ const distributedReportHistory = isDistributedReportHistoryExecution(clusterMode, this.options);
3572
+ let localReportInput = htmlReportHistoryRequested
3573
+ && distributedReportHistory
3574
+ ? (0, local_report_input_js_1.distributedLocalReportInput)()
3575
+ : (0, local_report_input_js_1.emptyLocalReportInput)();
3576
+ let reportHistoryCollector;
3577
+ let reportHistoryLifecycle;
3578
+ if (htmlReportHistoryRequested && !distributedReportHistory) {
3579
+ const warnReportHistoryFailure = (category, error) => {
3580
+ const exceptionClasses = error === undefined
3581
+ ? ""
3582
+ : `; exception_classes=${(0, report_history_js_1.sanitizedExceptionClassChain)(error)}`;
3583
+ try {
3584
+ runLogger.warn(`LoadStrike report history unavailable: ${category}${exceptionClasses}.`);
3585
+ }
3586
+ catch {
3587
+ // Report-only diagnostics are best-effort.
3588
+ }
3589
+ };
3590
+ reportHistoryCollector = new report_history_js_1.ReportHistoryCollector({
3591
+ scenarioCount: selectedScenarios.length,
3592
+ onCaptureError: (error) => warnReportHistoryFailure("capture_failure", error)
3593
+ });
3594
+ if (!reportHistoryCollector.available) {
3595
+ warnReportHistoryFailure("budget_pressure");
3596
+ }
3597
+ const snapshotFactory = () => Array.from(scenarioAccumulators.values())
3598
+ .map((value) => value.buildReportHistorySnapshot())
3599
+ .sort((left, right) => (left.sortIndex ?? 0) - (right.sortIndex ?? 0)
3600
+ || left.scenarioName.localeCompare(right.scenarioName));
3601
+ const reportHistoryWorker = new report_history_js_1.ReportHistoryWorker({
3602
+ collector: reportHistoryCollector,
3603
+ cadenceSeconds: resolvedReportHistoryCadenceSeconds(this.options.reportingIntervalSeconds ?? 5),
3604
+ snapshotFactory,
3605
+ onFailure: (category, exceptionClasses) => {
3606
+ try {
3607
+ runLogger.warn(`LoadStrike report history unavailable: ${category}; exception_classes=${exceptionClasses}.`);
3608
+ }
3609
+ catch {
3610
+ // Report-only diagnostics are best-effort.
3611
+ }
3612
+ }
3613
+ });
3614
+ reportHistoryLifecycle = new report_history_js_1.ReportHistoryLifecycleCoordinator({
3615
+ scenarioCount: selectedScenarios.length,
3616
+ start: () => reportHistoryWorker.start(),
3617
+ stopAndFinalize: () => reportHistoryWorker.stopAndFinalize(),
3618
+ stopWithoutFinalizing: () => reportHistoryWorker.stopWithoutFinalizing()
3619
+ });
3620
+ }
3510
3621
  const scenarioStartInfos = selectedScenarios.map((scenario, index) => {
3511
3622
  const startInfo = {
3512
3623
  scenarioName: scenario.name,
@@ -3550,14 +3661,14 @@ class LoadStrikeRunner {
3550
3661
  await init(baseContext, this.options.infraConfig ?? {});
3551
3662
  }
3552
3663
  }
3553
- await this.initializeSinks(sinkStates, baseContext, this.options.infraConfig ?? {}, sinkRetryCount, sinkRetryBackoffMs, sinkErrors);
3664
+ await this.initializeSinks(sinkStates, baseContext, this.options.infraConfig ?? {}, sinkRetryCount, sinkRetryBackoffMs, sinkErrors, testInfo.sessionId, runLogger);
3554
3665
  for (const plugin of plugins) {
3555
3666
  const start = resolveWorkerPluginStart(plugin);
3556
3667
  if (start) {
3557
3668
  await start(sessionInfo);
3558
3669
  }
3559
3670
  }
3560
- await this.startSinks(sinkStates, sessionInfo, sinkRetryCount, sinkRetryBackoffMs, sinkErrors);
3671
+ await this.startSinks(sinkStates, sessionInfo, sinkRetryCount, sinkRetryBackoffMs, sinkErrors, testInfo.sessionId, runLogger);
3561
3672
  const iterationObservationRunId = String(this.internalOptions.iterationObservationRunId
3562
3673
  ?? sessionInfo.portalReportingRunId
3563
3674
  ?? sessionInfo.PortalReportingRunId
@@ -3588,7 +3699,10 @@ class LoadStrikeRunner {
3588
3699
  expectedResultOwnerCount64: iterationObservationExpectedResultOwnerCount64,
3589
3700
  processGroup: iterationObservationProcessGroup,
3590
3701
  settings: resolveIterationObservationSettings(this.options),
3591
- sinks: reporterIterationObservationSinks
3702
+ sinks: reporterIterationObservationSinks,
3703
+ sinkRetryCount,
3704
+ sinkRetryBackoffMs,
3705
+ logger: runLogger
3592
3706
  });
3593
3707
  const emitRealtimeSnapshot = async () => {
3594
3708
  if (realtimeInFlight) {
@@ -3600,7 +3714,7 @@ class LoadStrikeRunner {
3600
3714
  .map((value) => value.build(scenarioDurationsMs.get(value.scenarioName) ?? Math.max(Date.now() - started.getTime(), 0)))
3601
3715
  .sort((left, right) => left.sortIndex - right.sortIndex);
3602
3716
  const metricsSnapshot = collectMetricStats(allRegisteredMetrics, Date.now() - started.getTime());
3603
- await this.emitRealtimeStats(sinkStates, snapshot, metricsSnapshot, sinkRetryCount, sinkRetryBackoffMs, sinkErrors);
3717
+ await this.emitRealtimeStats(sinkStates, snapshot, metricsSnapshot, sinkRetryCount, sinkRetryBackoffMs, sinkErrors, testInfo.sessionId, runLogger);
3604
3718
  if (toBoolean(this.options.displayConsoleMetrics, true)) {
3605
3719
  const requestCount = snapshot.reduce((sum, value) => sum + value.allRequestCount, 0);
3606
3720
  const okCount = snapshot.reduce((sum, value) => sum + value.allOkCount, 0);
@@ -3613,7 +3727,7 @@ class LoadStrikeRunner {
3613
3727
  realtimeInFlight = false;
3614
3728
  }
3615
3729
  };
3616
- const reportingIntervalMs = Math.max(Math.trunc((this.options.reportingIntervalSeconds ?? 5) * 1000), 1);
3730
+ const reportingIntervalMs = boundedReportingIntervalMs(this.options.reportingIntervalSeconds ?? 5);
3617
3731
  const triggerRealtimeSnapshot = () => {
3618
3732
  if (realtimeCurrent) {
3619
3733
  return;
@@ -3700,6 +3814,7 @@ class LoadStrikeRunner {
3700
3814
  iterationObservationRunId,
3701
3815
  iterationObservationResultOwnerId,
3702
3816
  iterationObservationProcessGroup,
3817
+ reportHistoryLifecycle,
3703
3818
  executeScenarioInvocation: (targetScenario, context, operation) => this.executeScenarioInvocation(targetScenario, context, operation),
3704
3819
  invokeBeforeScenario: (runtimePolicies, scenarioName) => this.invokeBeforeScenario(runtimePolicies, scenarioName, policyErrors, runtimePolicyErrorMode),
3705
3820
  invokeAfterScenario: (runtimePolicies, scenarioName, stats) => this.invokeAfterScenario(runtimePolicies, scenarioName, stats, policyErrors, runtimePolicyErrorMode),
@@ -3787,12 +3902,17 @@ class LoadStrikeRunner {
3787
3902
  ...iterationObservationReporter.buildWarnings()
3788
3903
  ];
3789
3904
  result.pluginsData = mergePluginData(result.pluginsData, await this.collectPluginData(plugins, attachRunResultAliases(result), pluginLifecycleErrors));
3905
+ if (reportHistoryCollector) {
3906
+ localReportInput = {
3907
+ history: reportHistoryCollector.toProjection()
3908
+ };
3909
+ }
3790
3910
  const finalizedResult = attachRunResultAliases(result);
3791
3911
  finalizedResult.logFiles = mergeStringArrays(finalizedResult.logFiles, loggerSetup.logFiles);
3792
- finalizedResult.reportFiles = this.writeReports(finalizedResult);
3912
+ finalizedResult.reportFiles = this.writeReports(finalizedResult, localReportInput);
3793
3913
  finalizedResult.disabledSinks = sinkStates.filter((x) => x.disabled).map((x) => x.name);
3794
- await this.emitRunResult(sinkStates, finalizedResult, sinkRetryCount, sinkRetryBackoffMs, sinkErrors);
3795
- await this.stopSinks(sinkStates, sinkRetryCount, sinkRetryBackoffMs, sinkErrors);
3914
+ await this.emitRunResult(sinkStates, finalizedResult, sinkRetryCount, sinkRetryBackoffMs, sinkErrors, testInfo.sessionId, runLogger);
3915
+ await this.stopSinks(sinkStates, sinkRetryCount, sinkRetryBackoffMs, sinkErrors, testInfo.sessionId, runLogger);
3796
3916
  sinksStopped = true;
3797
3917
  finalizedResult.disabledSinks = sinkStates.filter((x) => x.disabled).map((x) => x.name);
3798
3918
  finalizedResult.sinkErrors = sinkErrors
@@ -3800,12 +3920,13 @@ class LoadStrikeRunner {
3800
3920
  return finalizedResult;
3801
3921
  }
3802
3922
  finally {
3923
+ reportHistoryLifecycle?.stopWithoutFinalizing();
3803
3924
  await stopRealtimeReporting();
3804
3925
  if (!iterationObservationsFinalized) {
3805
3926
  await iterationObservationReporter.sealAndDrain().catch(() => { });
3806
3927
  }
3807
3928
  if (!sinksStopped) {
3808
- await this.stopSinks(sinkStates, sinkRetryCount, sinkRetryBackoffMs, sinkErrors);
3929
+ await this.stopSinks(sinkStates, sinkRetryCount, sinkRetryBackoffMs, sinkErrors, testInfo.sessionId, runLogger);
3809
3930
  }
3810
3931
  if (!pluginsStopped) {
3811
3932
  await this.stopPlugins(plugins, pluginLifecycleErrors, runLogger);
@@ -4113,9 +4234,9 @@ class LoadStrikeRunner {
4113
4234
  }
4114
4235
  return filtered;
4115
4236
  }
4116
- async initializeSinks(sinkStates, context, infraConfig, retryCount, retryBackoffMs, sinkErrors) {
4237
+ async initializeSinks(sinkStates, context, infraConfig, retryCount, retryBackoffMs, sinkErrors, runId, logger) {
4117
4238
  for (const state of sinkStates) {
4118
- await this.invokeSinkAction(state, "init", retryCount, retryBackoffMs, sinkErrors, false, true, async () => {
4239
+ await this.invokeSinkAction(state, "init", retryCount, retryBackoffMs, sinkErrors, runId, logger, false, true, async () => {
4119
4240
  const init = resolveSinkInit(state.sink);
4120
4241
  if (init) {
4121
4242
  await init(context, infraConfig);
@@ -4123,9 +4244,9 @@ class LoadStrikeRunner {
4123
4244
  });
4124
4245
  }
4125
4246
  }
4126
- async startSinks(sinkStates, session, retryCount, retryBackoffMs, sinkErrors) {
4247
+ async startSinks(sinkStates, session, retryCount, retryBackoffMs, sinkErrors, runId, logger) {
4127
4248
  for (const state of sinkStates) {
4128
- await this.invokeSinkAction(state, "start", retryCount, retryBackoffMs, sinkErrors, false, true, async () => {
4249
+ await this.invokeSinkAction(state, "start", retryCount, retryBackoffMs, sinkErrors, runId, logger, false, true, async () => {
4129
4250
  const start = resolveSinkStart(state.sink);
4130
4251
  if (start) {
4131
4252
  await start(session);
@@ -4133,9 +4254,9 @@ class LoadStrikeRunner {
4133
4254
  });
4134
4255
  }
4135
4256
  }
4136
- async emitRealtimeStats(sinkStates, scenarioStats, metrics, retryCount, retryBackoffMs, sinkErrors) {
4257
+ async emitRealtimeStats(sinkStates, scenarioStats, metrics, retryCount, retryBackoffMs, sinkErrors, runId, logger) {
4137
4258
  for (const state of sinkStates) {
4138
- await this.invokeSinkAction(state, "realtime", retryCount, retryBackoffMs, sinkErrors, false, true, async () => {
4259
+ await this.invokeSinkAction(state, "realtime", retryCount, retryBackoffMs, sinkErrors, runId, logger, false, true, async () => {
4139
4260
  const saveRealtimeStats = resolveSinkSaveRealtimeStats(state.sink);
4140
4261
  if (saveRealtimeStats) {
4141
4262
  await saveRealtimeStats(scenarioStats);
@@ -4147,10 +4268,9 @@ class LoadStrikeRunner {
4147
4268
  });
4148
4269
  }
4149
4270
  }
4150
- async stopSinks(sinkStates, retryCount, retryBackoffMs, sinkErrors) {
4151
- const shutdownRetryCount = 0;
4271
+ async stopSinks(sinkStates, retryCount, retryBackoffMs, sinkErrors, runId, logger) {
4152
4272
  for (const state of sinkStates) {
4153
- await this.invokeSinkAction(state, "stop", shutdownRetryCount, retryBackoffMs, sinkErrors, true, true, async () => {
4273
+ await this.invokeSinkAction(state, "stop", retryCount, retryBackoffMs, sinkErrors, runId, logger, true, true, async () => {
4154
4274
  const stop = resolveSinkStop(state.sink);
4155
4275
  if (stop) {
4156
4276
  await stop();
@@ -4158,15 +4278,15 @@ class LoadStrikeRunner {
4158
4278
  });
4159
4279
  const dispose = resolveSinkDispose(state.sink);
4160
4280
  if (dispose) {
4161
- await this.invokeSinkAction(state, "dispose", shutdownRetryCount, retryBackoffMs, sinkErrors, true, true, async () => {
4281
+ await this.invokeSinkAction(state, "dispose", retryCount, retryBackoffMs, sinkErrors, runId, logger, true, true, async () => {
4162
4282
  await dispose();
4163
4283
  });
4164
4284
  }
4165
4285
  }
4166
4286
  }
4167
- async emitRunResult(sinkStates, result, retryCount, retryBackoffMs, sinkErrors) {
4287
+ async emitRunResult(sinkStates, result, retryCount, retryBackoffMs, sinkErrors, runId, logger) {
4168
4288
  for (const state of sinkStates) {
4169
- await this.invokeSinkAction(state, "run-result", retryCount, retryBackoffMs, sinkErrors, false, false, async () => {
4289
+ await this.invokeSinkAction(state, "run-result", retryCount, retryBackoffMs, sinkErrors, runId, logger, false, false, async () => {
4170
4290
  const saveRunResult = resolveSinkSaveRunResult(state.sink);
4171
4291
  if (saveRunResult) {
4172
4292
  await saveRunResult(result);
@@ -4174,23 +4294,49 @@ class LoadStrikeRunner {
4174
4294
  });
4175
4295
  }
4176
4296
  }
4177
- async invokeSinkAction(state, phase, retryCount, retryBackoffMs, sinkErrors, ignoreDisabled, disableOnFailure, action) {
4297
+ async invokeSinkAction(state, phase, retryCount, retryBackoffMs, sinkErrors, runId, logger, ignoreDisabled, disableOnFailure, action) {
4178
4298
  if (state.disabled && !ignoreDisabled) {
4179
4299
  return;
4180
4300
  }
4181
- let attempts = 0;
4182
- while (attempts <= retryCount) {
4183
- attempts += 1;
4301
+ const maximumAttempts = (0, sink_retry_policy_js_1.normalizeSinkRetryCount)(retryCount) + 1;
4302
+ const backoffMs = (0, sink_retry_policy_js_1.normalizeSinkRetryBackoffMs)(retryBackoffMs);
4303
+ for (let attempts = 1; attempts <= maximumAttempts; attempts += 1) {
4184
4304
  try {
4185
4305
  await action();
4306
+ if (attempts > 1) {
4307
+ (0, iteration_observation_diagnostics_js_1.logIterationObservationRecovery)(logger, {
4308
+ sinkName: state.name,
4309
+ operation: "reporting-sink-action",
4310
+ phase,
4311
+ runId,
4312
+ resultOwnerId: "",
4313
+ observationCount: 0,
4314
+ attempt: attempts,
4315
+ maximumAttempts,
4316
+ nextDelayMs: 0
4317
+ });
4318
+ }
4186
4319
  return;
4187
4320
  }
4188
4321
  catch (error) {
4189
- if (attempts > retryCount) {
4322
+ const exhausted = attempts >= maximumAttempts;
4323
+ const nextDelayMs = exhausted ? 0 : (0, sink_retry_policy_js_1.sinkRetryDelayMs)(backoffMs, attempts);
4324
+ (0, iteration_observation_diagnostics_js_1.logIterationObservationFailure)(logger, exhausted ? "error" : "warn", {
4325
+ sinkName: state.name,
4326
+ operation: "reporting-sink-action",
4327
+ phase,
4328
+ runId,
4329
+ resultOwnerId: "",
4330
+ observationCount: 0,
4331
+ attempt: attempts,
4332
+ maximumAttempts,
4333
+ nextDelayMs
4334
+ }, error);
4335
+ if (exhausted) {
4190
4336
  sinkErrors.push({
4191
4337
  sinkName: state.name,
4192
4338
  phase,
4193
- message: String(error ?? "sink action failed"),
4339
+ message: "The reporting sink action failed after retries.",
4194
4340
  attempts
4195
4341
  });
4196
4342
  if (disableOnFailure) {
@@ -4198,9 +4344,7 @@ class LoadStrikeRunner {
4198
4344
  }
4199
4345
  return;
4200
4346
  }
4201
- if (retryBackoffMs > 0) {
4202
- await sleep(retryBackoffMs * attempts);
4203
- }
4347
+ await (0, sink_retry_policy_js_1.waitForSinkRetryDelay)(nextDelayMs);
4204
4348
  }
4205
4349
  }
4206
4350
  }
@@ -4254,7 +4398,7 @@ class LoadStrikeRunner {
4254
4398
  message: String(error ?? "runtime policy callback failed")
4255
4399
  });
4256
4400
  }
4257
- writeReports(result) {
4401
+ writeReports(result, localReportInput = (0, local_report_input_js_1.emptyLocalReportInput)()) {
4258
4402
  const reportsEnabled = this.options.reportsEnabled ?? true;
4259
4403
  if (!reportsEnabled) {
4260
4404
  return [];
@@ -4279,7 +4423,7 @@ class LoadStrikeRunner {
4279
4423
  (0, node_fs_1.writeFileSync)(path, (0, reporting_js_1.buildDotnetMarkdownReport)(nodeStats), "utf8");
4280
4424
  }
4281
4425
  else if (format === "html") {
4282
- (0, node_fs_1.writeFileSync)(path, (0, reporting_js_1.buildDotnetHtmlReport)(nodeStats), "utf8");
4426
+ (0, node_fs_1.writeFileSync)(path, (0, reporting_js_1.buildDotnetHtmlReport)(nodeStats, localReportInput), "utf8");
4283
4427
  }
4284
4428
  written.push(path);
4285
4429
  }
@@ -4313,6 +4457,19 @@ function hasPluginRows(value) {
4313
4457
  }
4314
4458
  return value.tables.some((table) => Array.isArray(table.rows) && table.rows.length > 0);
4315
4459
  }
4460
+ function boundedReportingIntervalMs(seconds) {
4461
+ if (!Number.isFinite(seconds) || seconds <= 0) {
4462
+ return 1;
4463
+ }
4464
+ const milliseconds = seconds * 1000;
4465
+ if (!Number.isFinite(milliseconds) || milliseconds >= 2147483647) {
4466
+ return 2147483647;
4467
+ }
4468
+ return Math.max(Math.trunc(milliseconds), 1);
4469
+ }
4470
+ function resolvedReportHistoryCadenceSeconds(seconds) {
4471
+ return boundedReportingIntervalMs(seconds) / 1000;
4472
+ }
4316
4473
  async function executeV2FixedArrivals(args) {
4317
4474
  const { rate, intervalNs, totalArrivals, budget, cancellationToken, shouldStopNow, nextInstanceInfo, runBombingInvocation, logger, scenarioName, deadlineOffsetsNs, tolerancesNs, ownedOrdinals, shardIndex = 0, shardCount = 1, telemetry, segment } = args;
4318
4475
  const segmentStartNs = process.hrtime.bigint();
@@ -4449,7 +4606,15 @@ function maxBigInt(left, right) {
4449
4606
  return left > right ? left : right;
4450
4607
  }
4451
4608
  async function executeScenarioRuntime(args) {
4452
- 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;
4609
+ 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;
4610
+ let reportHistoryExecutionEnded = false;
4611
+ const endReportHistoryExecution = async () => {
4612
+ if (reportHistoryExecutionEnded) {
4613
+ return;
4614
+ }
4615
+ reportHistoryExecutionEnded = true;
4616
+ await reportHistoryLifecycle?.scenarioExecutionEnded();
4617
+ };
4453
4618
  const scenarioStartedMs = Date.now();
4454
4619
  const scenarioContextData = {};
4455
4620
  const registeredMetrics = [];
@@ -4610,7 +4775,18 @@ async function executeScenarioRuntime(args) {
4610
4775
  attachScenarioContextAliases(context);
4611
4776
  const startedUtcNs = (0, iteration_observations_js_1.utcNowNs)();
4612
4777
  const startedAtNs = process.hrtime.bigint();
4613
- const reply = await executeScenarioInvocation(scenario, context, operation);
4778
+ let policyFailure;
4779
+ let reply;
4780
+ try {
4781
+ reply = await executeScenarioInvocation(scenario, context, operation);
4782
+ }
4783
+ catch (error) {
4784
+ if (!(error instanceof RuntimePolicyCallbackError)) {
4785
+ throw error;
4786
+ }
4787
+ policyFailure = error;
4788
+ reply = LoadStrikeResponse.fail("runtime_policy_error", "", 0);
4789
+ }
4614
4790
  const observedLatencyNs = maxBigInt(process.hrtime.bigint() - startedAtNs, 0n);
4615
4791
  const completedUtcNs = startedUtcNs + observedLatencyNs;
4616
4792
  const observedLatencyMs = Number(observedLatencyNs) / 1000000;
@@ -4625,7 +4801,8 @@ async function executeScenarioRuntime(args) {
4625
4801
  observedLatencyUs: observedLatencyNs / 1000n,
4626
4802
  reportedLatencyUs: normalizeRawObservationLatencyMicroseconds(resolveRecordedLatency(reply.customLatencyMs, observedLatencyMs)),
4627
4803
  steps: attemptSteps,
4628
- recordedSteps
4804
+ recordedSteps,
4805
+ ...(policyFailure ? { policyFailure } : {})
4629
4806
  };
4630
4807
  };
4631
4808
  const captureAttemptObservation = (operation, globalOrdinal, attemptIndex, isFinalAttempt, attempt, simulationIndex, simulationKind, iterationId, globalSecondaryOrdinal = 0n) => {
@@ -4674,22 +4851,18 @@ async function executeScenarioRuntime(args) {
4674
4851
  const maxAttempts = 1 + (scenario.shouldRestartIterationOnFail() ? restartIterationMaxAttempts : 0);
4675
4852
  while (attempts < maxAttempts && !shouldStopNow()) {
4676
4853
  attempts += 1;
4677
- let attempt;
4678
- try {
4679
- attempt = await runSingleInvocation("Bombing", instanceData, instanceNumber, instanceId, false);
4680
- }
4681
- catch (error) {
4682
- if (error instanceof RuntimePolicyCallbackError) {
4683
- stopScenario = true;
4684
- scenarioAbortController.abort(error);
4685
- }
4686
- throw error;
4687
- }
4688
- const shouldRetry = !attempt.reply.isSuccess
4854
+ const attempt = await runSingleInvocation("Bombing", instanceData, instanceNumber, instanceId, false);
4855
+ const shouldRetry = !attempt.policyFailure
4856
+ && !attempt.reply.isSuccess
4689
4857
  && scenario.shouldRestartIterationOnFail()
4690
4858
  && attempts < maxAttempts
4691
4859
  && !shouldStopNow();
4692
4860
  captureAttemptObservation("Bombing", globalOrdinal, attempts - 1, !shouldRetry, attempt, simulationIndex, simulationKind, iterationId, explicitSecondaryOrdinal);
4861
+ if (attempt.policyFailure) {
4862
+ stopScenario = true;
4863
+ scenarioAbortController.abort(attempt.policyFailure);
4864
+ throw attempt.policyFailure;
4865
+ }
4693
4866
  if (!shouldRetry) {
4694
4867
  for (const step of attempt.recordedSteps) {
4695
4868
  recordStepReply(step.stepName, step.reply, step.observedLatencyMs, step.sortIndex);
@@ -4716,6 +4889,11 @@ async function executeScenarioRuntime(args) {
4716
4889
  const globalOrdinal = nextObservationOrdinal();
4717
4890
  const attempt = await runSingleInvocation("WarmUp", instanceInfo.instanceData, instanceInfo.instanceNumber, instanceInfo.instanceId, false);
4718
4891
  captureAttemptObservation("WarmUp", globalOrdinal, 0, true, attempt, -1, "SingleInvocation");
4892
+ if (attempt.policyFailure) {
4893
+ stopScenario = true;
4894
+ scenarioAbortController.abort(attempt.policyFailure);
4895
+ throw attempt.policyFailure;
4896
+ }
4719
4897
  }
4720
4898
  };
4721
4899
  const executeV2TimedConstant = async (copies, durationNs, ramping, runSimulationInvocation, segment) => {
@@ -5257,6 +5435,7 @@ async function executeScenarioRuntime(args) {
5257
5435
  })));
5258
5436
  await invokeBeforeScenario(policies, scenario.name);
5259
5437
  await runWarmUpAsync();
5438
+ reportHistoryLifecycle?.scenarioBombingStarted();
5260
5439
  accumulator.setCurrentOperation("Bombing");
5261
5440
  const simulations = scenario.getSimulations();
5262
5441
  if (!simulations.length) {
@@ -5290,6 +5469,7 @@ async function executeScenarioRuntime(args) {
5290
5469
  }
5291
5470
  }
5292
5471
  accumulator.setCurrentOperation(stopScenario || stopTestState.value ? "Stop" : "Complete");
5472
+ await endReportHistoryExecution();
5293
5473
  await invokeAfterScenario(policies, scenario.name, runtime);
5294
5474
  }
5295
5475
  catch (error) {
@@ -5306,6 +5486,7 @@ async function executeScenarioRuntime(args) {
5306
5486
  }
5307
5487
  }
5308
5488
  finally {
5489
+ await endReportHistoryExecution();
5309
5490
  scenarioDurationsMs.set(scenario.name, Math.max(Date.now() - scenarioStartedMs, 0));
5310
5491
  try {
5311
5492
  await scenario.invokeClean({
@@ -5386,10 +5567,17 @@ async function waitForScenarioTasks(tasks, scenarioName, timeoutSeconds, logger,
5386
5567
  throwPolicyFailure(await settled);
5387
5568
  return;
5388
5569
  }
5389
- const completed = await Promise.race([
5390
- settled.then(() => true),
5391
- delayWithAbort(Math.trunc(timeoutSeconds * 1000), signal).then(() => false)
5392
- ]);
5570
+ const timeoutController = new AbortController();
5571
+ let completed;
5572
+ try {
5573
+ completed = await Promise.race([
5574
+ settled.then(() => true),
5575
+ delayWithAbort(Math.trunc(timeoutSeconds * 1000), combineAbortSignals(signal, timeoutController.signal)).then(() => false)
5576
+ ]);
5577
+ }
5578
+ finally {
5579
+ timeoutController.abort();
5580
+ }
5393
5581
  if (!completed) {
5394
5582
  if (signal.reason instanceof RuntimePolicyCallbackError) {
5395
5583
  throw signal.reason;
@@ -7232,6 +7420,11 @@ function resolveClusterExecutionMode(options) {
7232
7420
  }
7233
7421
  return "single";
7234
7422
  }
7423
+ function isDistributedReportHistoryExecution(clusterMode, options) {
7424
+ return clusterMode !== "single"
7425
+ || options.loadEngineV2SegmentLifecycleOverride !== undefined
7426
+ || Math.max(Math.trunc(options.clusterShardCount ?? 1), 1) > 1;
7427
+ }
7235
7428
  function buildEmptyNodeStats(args) {
7236
7429
  return attachNodeStatsAliases({
7237
7430
  startedUtc: args.startedUtc,
@@ -9451,11 +9644,23 @@ function readRuntimeTrackingId(payload, selector) {
9451
9644
  return null;
9452
9645
  }
9453
9646
  let current = body;
9454
- for (const segment of selector.slice("json:".length).trim().replace(/^\$\./, "").split(".").filter(Boolean)) {
9647
+ const path = selector.slice("json:".length).trim().replace(/^\$\./, "");
9648
+ let segments;
9649
+ try {
9650
+ segments = runtimeSafeJsonPathSegments(path);
9651
+ }
9652
+ catch {
9653
+ return null;
9654
+ }
9655
+ for (const segment of segments) {
9455
9656
  if (!current || typeof current !== "object" || Array.isArray(current)) {
9456
9657
  return null;
9457
9658
  }
9458
- current = current[segment];
9659
+ const record = current;
9660
+ if (!Object.prototype.hasOwnProperty.call(record, segment)) {
9661
+ return null;
9662
+ }
9663
+ current = runtimeReadOwnJsonProperty(record, segment);
9459
9664
  }
9460
9665
  return current == null ? null : String(current);
9461
9666
  }
@@ -9479,23 +9684,57 @@ function runtimeParseBodyAsObject(body) {
9479
9684
  }
9480
9685
  }
9481
9686
  function setRuntimeJsonPathValue(body, path, value) {
9482
- const target = body && typeof body === "object" && !Array.isArray(body)
9483
- ? { ...body }
9484
- : {};
9485
- const segments = path.split(".").filter(Boolean);
9687
+ const target = runtimeCloneJsonRecord(body);
9688
+ const segments = runtimeSafeJsonPathSegments(path);
9486
9689
  if (!segments.length) {
9487
9690
  return target;
9488
9691
  }
9489
9692
  let current = target;
9490
9693
  for (let i = 0; i < segments.length - 1; i += 1) {
9491
9694
  const segment = segments[i];
9492
- const next = current[segment];
9695
+ const next = runtimeReadOwnJsonProperty(current, segment);
9696
+ let child;
9493
9697
  if (!next || typeof next !== "object" || Array.isArray(next)) {
9494
- current[segment] = {};
9698
+ child = {};
9495
9699
  }
9496
- current = current[segment];
9700
+ else {
9701
+ child = runtimeCloneJsonRecord(next);
9702
+ }
9703
+ runtimeDefineJsonProperty(current, segment, child);
9704
+ current = child;
9705
+ }
9706
+ runtimeDefineJsonProperty(current, segments[segments.length - 1], value);
9707
+ return target;
9708
+ }
9709
+ const FORBIDDEN_RUNTIME_JSON_PATH_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
9710
+ function runtimeSafeJsonPathSegments(path) {
9711
+ const segments = path.split(".").filter(Boolean);
9712
+ const forbidden = segments.find((segment) => FORBIDDEN_RUNTIME_JSON_PATH_SEGMENTS.has(segment));
9713
+ if (forbidden) {
9714
+ throw new Error(`Tracking selector contains forbidden JSON path segment '${forbidden}'.`);
9715
+ }
9716
+ return segments;
9717
+ }
9718
+ function runtimeDefineJsonProperty(target, key, value) {
9719
+ Object.defineProperty(target, key, {
9720
+ configurable: true,
9721
+ enumerable: true,
9722
+ value,
9723
+ writable: true
9724
+ });
9725
+ }
9726
+ function runtimeReadOwnJsonProperty(target, key) {
9727
+ const descriptor = Object.getOwnPropertyDescriptor(target, key);
9728
+ return descriptor && "value" in descriptor ? descriptor.value : undefined;
9729
+ }
9730
+ function runtimeCloneJsonRecord(value) {
9731
+ const target = {};
9732
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
9733
+ return target;
9734
+ }
9735
+ for (const [key, entry] of Object.entries(value)) {
9736
+ runtimeDefineJsonProperty(target, key, entry);
9497
9737
  }
9498
- current[segments[segments.length - 1]] = value;
9499
9738
  return target;
9500
9739
  }
9501
9740
  function asTrackingRecord(value) {
@@ -9505,8 +9744,8 @@ function asTrackingRecord(value) {
9505
9744
  }
9506
9745
  function pickTrackingValue(source, ...keys) {
9507
9746
  for (const key of keys) {
9508
- if (key in source) {
9509
- return source[key];
9747
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
9748
+ return runtimeReadOwnJsonProperty(source, key);
9510
9749
  }
9511
9750
  }
9512
9751
  return undefined;
@@ -9760,26 +9999,10 @@ function createDefaultLogger(logFilePath) {
9760
9999
  function wrapLoggerWithMinimumLevel(baseLogger, minimumLogLevel) {
9761
10000
  const threshold = logLevelOrder(minimumLogLevel);
9762
10001
  return {
9763
- debug: (message) => {
9764
- if (threshold <= 0) {
9765
- baseLogger.debug(message);
9766
- }
9767
- },
9768
- info: (message) => {
9769
- if (threshold <= 1) {
9770
- baseLogger.info(message);
9771
- }
9772
- },
9773
- warn: (message) => {
9774
- if (threshold <= 2) {
9775
- baseLogger.warn(message);
9776
- }
9777
- },
9778
- error: (message) => {
9779
- if (threshold <= 3) {
9780
- baseLogger.error(message);
9781
- }
9782
- }
10002
+ debug: (message) => threshold <= 0 ? baseLogger.debug(message) : undefined,
10003
+ info: (message) => threshold <= 1 ? baseLogger.info(message) : undefined,
10004
+ warn: (message) => threshold <= 2 ? baseLogger.warn(message) : undefined,
10005
+ error: (message) => threshold <= 3 ? baseLogger.error(message) : undefined
9783
10006
  };
9784
10007
  }
9785
10008
  function formatDefaultLoggerLine(level, message) {
@@ -11033,6 +11256,7 @@ function buildGroupedCorrelationRows(rows) {
11033
11256
  LatencyMinMs: latencySamples.length ? formatOptionalLatency(latencySamples[0]) : "",
11034
11257
  LatencyMeanMs: latencySamples.length ? formatOptionalLatency(latencySamples.reduce((sum, value) => sum + value, 0) / latencySamples.length) : "",
11035
11258
  LatencyP50Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.5)) : "",
11259
+ LatencyP75Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.75)) : "",
11036
11260
  LatencyP80Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.8)) : "",
11037
11261
  LatencyP85Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.85)) : "",
11038
11262
  LatencyP90Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.9)) : "",
@@ -11245,6 +11469,8 @@ exports.__loadstrikeTestExports = {
11245
11469
  buildThresholdCheckExpression,
11246
11470
  buildTrackingLeaseKey,
11247
11471
  buildTrackingRunNamespace,
11472
+ boundedReportingIntervalMs,
11473
+ resolvedReportHistoryCadenceSeconds,
11248
11474
  clusterNodeResultToNodeStats,
11249
11475
  combineAbortSignals,
11250
11476
  computeScenarioRequestCount,
@@ -11264,6 +11490,7 @@ exports.__loadstrikeTestExports = {
11264
11490
  formatUtcReportTimestamp,
11265
11491
  hasPluginRows,
11266
11492
  inferRuntimeLegacyHttpResponseSource,
11493
+ isDistributedReportHistoryExecution,
11267
11494
  isComparisonFailed,
11268
11495
  loadJsonObject,
11269
11496
  logLevelOrder,
@@ -11293,6 +11520,7 @@ exports.__loadstrikeTestExports = {
11293
11520
  parseStrictBooleanToken,
11294
11521
  percentile,
11295
11522
  pickOptionalTrackingSelectorString,
11523
+ pickTrackingValue,
11296
11524
  pickTrackingNumber,
11297
11525
  produceOrConsumeTrackingPayload,
11298
11526
  readConfiguredSinkName,