@loadstrike/loadstrike-sdk 1.0.31001 → 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.
@@ -7,6 +7,8 @@ import { DistributedClusterAgent, DistributedClusterCoordinator, buildLoadEngine
7
7
  import { CorrelationStoreConfiguration, CrossPlatformTrackingRuntime, RedisCorrelationStore, RedisCorrelationStoreOptions, TrackingFieldSelector } from "./correlation.js";
8
8
  import { EndpointAdapterFactory, LOADSTRIKE_TRACE_ID_TRACKING_FIELD } from "./transports.js";
9
9
  import { buildDotnetCsvReport, buildDotnetHtmlReport, buildDotnetMarkdownReport, buildDotnetTxtReport } from "./reporting.js";
10
+ import { ReportHistoryCollector, ReportHistoryLifecycleCoordinator, ReportHistoryWorker, sanitizedExceptionClassChain } from "./report-history.js";
11
+ import { distributedLocalReportInput, emptyLocalReportInput } from "./local-report-input.js";
10
12
  import { PortalReportingSink, cloneReportingSinkForRun } from "./sinks.js";
11
13
  import { LoadEngineV2ExecutionBudget, buildLoadEngineV2TrafficMixSeedId, LoadStrikeHistogramV1, parseLoadEngineV2HistogramArtifact, serializeLoadEngineV2HistogramArtifact, classifyLoadEngineV2Arrival, loadEngineV2FixedArrivalCount, loadEngineV2FixedDeadlineNs, loadEngineV2LatenessToleranceNs, loadEngineV2TrafficMixLaneUnitCount, loadEngineV2TrafficMixOwnedUnits, planRampingInjectionDeadlines, planRampingConstantDeadlines, planRandomInjectionDeadlines, fnv1a32 } from "./load-engine-v2.js";
12
14
  import { DEFAULT_ITERATION_OBSERVATION_SETTINGS, IterationObservationReporter, createIterationObservation, createIterationStepObservation, utcNowNs, validateIterationObservationSettings } from "./iteration-observations.js";
@@ -283,6 +285,28 @@ class MeasurementAccumulator {
283
285
  moreOrEq1200: left.moreOrEq1200 + right.moreOrEq1200
284
286
  }, allRequestCount, durationMs);
285
287
  }
288
+ /**
289
+ * Projects only the cumulative fields used by the private HTML-report
290
+ * history. Legacy distributions deliberately omit latency so this cadence
291
+ * path never scans their retained per-request arrays.
292
+ */
293
+ buildReportHistoryMeasurement() {
294
+ return projectNativeReportHistoryMeasurement(this.count, this.allBytes, this.useHistogram ? this.latencyHistogram : undefined);
295
+ }
296
+ /**
297
+ * Combines only bounded native latency state plus exact counters. It does
298
+ * not materialize status rows or any public measurement DTO.
299
+ */
300
+ buildCombinedReportHistoryMeasurement(other) {
301
+ const count = this.count + other.count;
302
+ const bytes = this.allBytes + other.allBytes;
303
+ if (!this.useHistogram || !other.useHistogram) {
304
+ return projectNativeReportHistoryMeasurement(count, bytes);
305
+ }
306
+ const latency = this.latencyHistogram.clone();
307
+ latency.merge(other.latencyHistogram);
308
+ return projectNativeReportHistoryMeasurement(count, bytes, latency);
309
+ }
286
310
  histogramSnapshot() {
287
311
  return {
288
312
  count: this.count,
@@ -296,6 +320,24 @@ class MeasurementAccumulator {
296
320
  };
297
321
  }
298
322
  }
323
+ function projectNativeReportHistoryMeasurement(count, bytes, latency) {
324
+ const measurement = {
325
+ count,
326
+ bytes,
327
+ approximate: latency?.maxRelativeError !== undefined
328
+ && latency.maxRelativeError > 0
329
+ };
330
+ if (!latency || latency.count === 0n) {
331
+ return measurement;
332
+ }
333
+ return {
334
+ ...measurement,
335
+ percent50Ms: Number(latency.percentile(0.5)) / 1000,
336
+ percent75Ms: Number(latency.percentile(0.75)) / 1000,
337
+ percent95Ms: Number(latency.percentile(0.95)) / 1000,
338
+ percent99Ms: Number(latency.percentile(0.99)) / 1000
339
+ };
340
+ }
299
341
  function buildHistogramMeasurement(snapshot, allRequestCount, durationMs) {
300
342
  const count = snapshot.count;
301
343
  const totalDurationMs = Math.max(durationMs, 0);
@@ -634,6 +676,21 @@ class ScenarioStatsAccumulator {
634
676
  step.record(reply, observedLatencyMs);
635
677
  return step.sortIndex;
636
678
  }
679
+ /**
680
+ * Captures the bounded, private history view without building scenario
681
+ * steps, status rows, plugins, aliases, or any other public result shape.
682
+ */
683
+ buildReportHistorySnapshot() {
684
+ const ok = this.ok.buildReportHistoryMeasurement();
685
+ const failed = this.fail.buildReportHistoryMeasurement();
686
+ return {
687
+ scenarioName: this.scenarioName,
688
+ sortIndex: this.sortIndex,
689
+ all: this.ok.buildCombinedReportHistoryMeasurement(this.fail),
690
+ ...(ok.count > 0 ? { ok } : {}),
691
+ ...(failed.count > 0 ? { failed } : {})
692
+ };
693
+ }
637
694
  /**
638
695
  * Builds the configured payload or helper object.
639
696
  * Use this when all builder inputs are ready to be materialized.
@@ -3481,6 +3538,58 @@ export class LoadStrikeRunner {
3481
3538
  licenseSession = await licenseClient.acquireLicenseLease(licensePayload);
3482
3539
  const loggerSetup = createLoggerSetup(this.options.loggerConfig, this.options.minimumLogLevel, this.options, testInfo, nodeInfo);
3483
3540
  const runLogger = loggerSetup.logger;
3541
+ const htmlReportHistoryRequested = (this.options.reportsEnabled ?? true)
3542
+ && normalizeReportFormats(this.options.reportFormats ?? ["html", "txt", "csv", "md"]).includes("html");
3543
+ const distributedReportHistory = isDistributedReportHistoryExecution(clusterMode, this.options);
3544
+ let localReportInput = htmlReportHistoryRequested
3545
+ && distributedReportHistory
3546
+ ? distributedLocalReportInput()
3547
+ : emptyLocalReportInput();
3548
+ let reportHistoryCollector;
3549
+ let reportHistoryLifecycle;
3550
+ if (htmlReportHistoryRequested && !distributedReportHistory) {
3551
+ const warnReportHistoryFailure = (category, error) => {
3552
+ const exceptionClasses = error === undefined
3553
+ ? ""
3554
+ : `; exception_classes=${sanitizedExceptionClassChain(error)}`;
3555
+ try {
3556
+ runLogger.warn(`LoadStrike report history unavailable: ${category}${exceptionClasses}.`);
3557
+ }
3558
+ catch {
3559
+ // Report-only diagnostics are best-effort.
3560
+ }
3561
+ };
3562
+ reportHistoryCollector = new ReportHistoryCollector({
3563
+ scenarioCount: selectedScenarios.length,
3564
+ onCaptureError: (error) => warnReportHistoryFailure("capture_failure", error)
3565
+ });
3566
+ if (!reportHistoryCollector.available) {
3567
+ warnReportHistoryFailure("budget_pressure");
3568
+ }
3569
+ const snapshotFactory = () => Array.from(scenarioAccumulators.values())
3570
+ .map((value) => value.buildReportHistorySnapshot())
3571
+ .sort((left, right) => (left.sortIndex ?? 0) - (right.sortIndex ?? 0)
3572
+ || left.scenarioName.localeCompare(right.scenarioName));
3573
+ const reportHistoryWorker = new ReportHistoryWorker({
3574
+ collector: reportHistoryCollector,
3575
+ cadenceSeconds: resolvedReportHistoryCadenceSeconds(this.options.reportingIntervalSeconds ?? 5),
3576
+ snapshotFactory,
3577
+ onFailure: (category, exceptionClasses) => {
3578
+ try {
3579
+ runLogger.warn(`LoadStrike report history unavailable: ${category}; exception_classes=${exceptionClasses}.`);
3580
+ }
3581
+ catch {
3582
+ // Report-only diagnostics are best-effort.
3583
+ }
3584
+ }
3585
+ });
3586
+ reportHistoryLifecycle = new ReportHistoryLifecycleCoordinator({
3587
+ scenarioCount: selectedScenarios.length,
3588
+ start: () => reportHistoryWorker.start(),
3589
+ stopAndFinalize: () => reportHistoryWorker.stopAndFinalize(),
3590
+ stopWithoutFinalizing: () => reportHistoryWorker.stopWithoutFinalizing()
3591
+ });
3592
+ }
3484
3593
  const scenarioStartInfos = selectedScenarios.map((scenario, index) => {
3485
3594
  const startInfo = {
3486
3595
  scenarioName: scenario.name,
@@ -3590,7 +3699,7 @@ export class LoadStrikeRunner {
3590
3699
  realtimeInFlight = false;
3591
3700
  }
3592
3701
  };
3593
- const reportingIntervalMs = Math.max(Math.trunc((this.options.reportingIntervalSeconds ?? 5) * 1000), 1);
3702
+ const reportingIntervalMs = boundedReportingIntervalMs(this.options.reportingIntervalSeconds ?? 5);
3594
3703
  const triggerRealtimeSnapshot = () => {
3595
3704
  if (realtimeCurrent) {
3596
3705
  return;
@@ -3677,6 +3786,7 @@ export class LoadStrikeRunner {
3677
3786
  iterationObservationRunId,
3678
3787
  iterationObservationResultOwnerId,
3679
3788
  iterationObservationProcessGroup,
3789
+ reportHistoryLifecycle,
3680
3790
  executeScenarioInvocation: (targetScenario, context, operation) => this.executeScenarioInvocation(targetScenario, context, operation),
3681
3791
  invokeBeforeScenario: (runtimePolicies, scenarioName) => this.invokeBeforeScenario(runtimePolicies, scenarioName, policyErrors, runtimePolicyErrorMode),
3682
3792
  invokeAfterScenario: (runtimePolicies, scenarioName, stats) => this.invokeAfterScenario(runtimePolicies, scenarioName, stats, policyErrors, runtimePolicyErrorMode),
@@ -3764,9 +3874,14 @@ export class LoadStrikeRunner {
3764
3874
  ...iterationObservationReporter.buildWarnings()
3765
3875
  ];
3766
3876
  result.pluginsData = mergePluginData(result.pluginsData, await this.collectPluginData(plugins, attachRunResultAliases(result), pluginLifecycleErrors));
3877
+ if (reportHistoryCollector) {
3878
+ localReportInput = {
3879
+ history: reportHistoryCollector.toProjection()
3880
+ };
3881
+ }
3767
3882
  const finalizedResult = attachRunResultAliases(result);
3768
3883
  finalizedResult.logFiles = mergeStringArrays(finalizedResult.logFiles, loggerSetup.logFiles);
3769
- finalizedResult.reportFiles = this.writeReports(finalizedResult);
3884
+ finalizedResult.reportFiles = this.writeReports(finalizedResult, localReportInput);
3770
3885
  finalizedResult.disabledSinks = sinkStates.filter((x) => x.disabled).map((x) => x.name);
3771
3886
  await this.emitRunResult(sinkStates, finalizedResult, sinkRetryCount, sinkRetryBackoffMs, sinkErrors, testInfo.sessionId, runLogger);
3772
3887
  await this.stopSinks(sinkStates, sinkRetryCount, sinkRetryBackoffMs, sinkErrors, testInfo.sessionId, runLogger);
@@ -3777,6 +3892,7 @@ export class LoadStrikeRunner {
3777
3892
  return finalizedResult;
3778
3893
  }
3779
3894
  finally {
3895
+ reportHistoryLifecycle?.stopWithoutFinalizing();
3780
3896
  await stopRealtimeReporting();
3781
3897
  if (!iterationObservationsFinalized) {
3782
3898
  await iterationObservationReporter.sealAndDrain().catch(() => { });
@@ -4254,7 +4370,7 @@ export class LoadStrikeRunner {
4254
4370
  message: String(error ?? "runtime policy callback failed")
4255
4371
  });
4256
4372
  }
4257
- writeReports(result) {
4373
+ writeReports(result, localReportInput = emptyLocalReportInput()) {
4258
4374
  const reportsEnabled = this.options.reportsEnabled ?? true;
4259
4375
  if (!reportsEnabled) {
4260
4376
  return [];
@@ -4279,7 +4395,7 @@ export class LoadStrikeRunner {
4279
4395
  writeFileSync(path, buildDotnetMarkdownReport(nodeStats), "utf8");
4280
4396
  }
4281
4397
  else if (format === "html") {
4282
- writeFileSync(path, buildDotnetHtmlReport(nodeStats), "utf8");
4398
+ writeFileSync(path, buildDotnetHtmlReport(nodeStats, localReportInput), "utf8");
4283
4399
  }
4284
4400
  written.push(path);
4285
4401
  }
@@ -4312,6 +4428,19 @@ function hasPluginRows(value) {
4312
4428
  }
4313
4429
  return value.tables.some((table) => Array.isArray(table.rows) && table.rows.length > 0);
4314
4430
  }
4431
+ function boundedReportingIntervalMs(seconds) {
4432
+ if (!Number.isFinite(seconds) || seconds <= 0) {
4433
+ return 1;
4434
+ }
4435
+ const milliseconds = seconds * 1000;
4436
+ if (!Number.isFinite(milliseconds) || milliseconds >= 2147483647) {
4437
+ return 2147483647;
4438
+ }
4439
+ return Math.max(Math.trunc(milliseconds), 1);
4440
+ }
4441
+ function resolvedReportHistoryCadenceSeconds(seconds) {
4442
+ return boundedReportingIntervalMs(seconds) / 1000;
4443
+ }
4315
4444
  async function executeV2FixedArrivals(args) {
4316
4445
  const { rate, intervalNs, totalArrivals, budget, cancellationToken, shouldStopNow, nextInstanceInfo, runBombingInvocation, logger, scenarioName, deadlineOffsetsNs, tolerancesNs, ownedOrdinals, shardIndex = 0, shardCount = 1, telemetry, segment } = args;
4317
4446
  const segmentStartNs = process.hrtime.bigint();
@@ -4448,7 +4577,15 @@ function maxBigInt(left, right) {
4448
4577
  return left > right ? left : right;
4449
4578
  }
4450
4579
  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;
4580
+ 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;
4581
+ let reportHistoryExecutionEnded = false;
4582
+ const endReportHistoryExecution = async () => {
4583
+ if (reportHistoryExecutionEnded) {
4584
+ return;
4585
+ }
4586
+ reportHistoryExecutionEnded = true;
4587
+ await reportHistoryLifecycle?.scenarioExecutionEnded();
4588
+ };
4452
4589
  const scenarioStartedMs = Date.now();
4453
4590
  const scenarioContextData = {};
4454
4591
  const registeredMetrics = [];
@@ -5269,6 +5406,7 @@ async function executeScenarioRuntime(args) {
5269
5406
  })));
5270
5407
  await invokeBeforeScenario(policies, scenario.name);
5271
5408
  await runWarmUpAsync();
5409
+ reportHistoryLifecycle?.scenarioBombingStarted();
5272
5410
  accumulator.setCurrentOperation("Bombing");
5273
5411
  const simulations = scenario.getSimulations();
5274
5412
  if (!simulations.length) {
@@ -5302,6 +5440,7 @@ async function executeScenarioRuntime(args) {
5302
5440
  }
5303
5441
  }
5304
5442
  accumulator.setCurrentOperation(stopScenario || stopTestState.value ? "Stop" : "Complete");
5443
+ await endReportHistoryExecution();
5305
5444
  await invokeAfterScenario(policies, scenario.name, runtime);
5306
5445
  }
5307
5446
  catch (error) {
@@ -5318,6 +5457,7 @@ async function executeScenarioRuntime(args) {
5318
5457
  }
5319
5458
  }
5320
5459
  finally {
5460
+ await endReportHistoryExecution();
5321
5461
  scenarioDurationsMs.set(scenario.name, Math.max(Date.now() - scenarioStartedMs, 0));
5322
5462
  try {
5323
5463
  await scenario.invokeClean({
@@ -7251,6 +7391,11 @@ function resolveClusterExecutionMode(options) {
7251
7391
  }
7252
7392
  return "single";
7253
7393
  }
7394
+ function isDistributedReportHistoryExecution(clusterMode, options) {
7395
+ return clusterMode !== "single"
7396
+ || options.loadEngineV2SegmentLifecycleOverride !== undefined
7397
+ || Math.max(Math.trunc(options.clusterShardCount ?? 1), 1) > 1;
7398
+ }
7254
7399
  function buildEmptyNodeStats(args) {
7255
7400
  return attachNodeStatsAliases({
7256
7401
  startedUtc: args.startedUtc,
@@ -11082,6 +11227,7 @@ function buildGroupedCorrelationRows(rows) {
11082
11227
  LatencyMinMs: latencySamples.length ? formatOptionalLatency(latencySamples[0]) : "",
11083
11228
  LatencyMeanMs: latencySamples.length ? formatOptionalLatency(latencySamples.reduce((sum, value) => sum + value, 0) / latencySamples.length) : "",
11084
11229
  LatencyP50Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.5)) : "",
11230
+ LatencyP75Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.75)) : "",
11085
11231
  LatencyP80Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.8)) : "",
11086
11232
  LatencyP85Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.85)) : "",
11087
11233
  LatencyP90Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.9)) : "",
@@ -11294,6 +11440,8 @@ export const __loadstrikeTestExports = {
11294
11440
  buildThresholdCheckExpression,
11295
11441
  buildTrackingLeaseKey,
11296
11442
  buildTrackingRunNamespace,
11443
+ boundedReportingIntervalMs,
11444
+ resolvedReportHistoryCadenceSeconds,
11297
11445
  clusterNodeResultToNodeStats,
11298
11446
  combineAbortSignals,
11299
11447
  computeScenarioRequestCount,
@@ -11313,6 +11461,7 @@ export const __loadstrikeTestExports = {
11313
11461
  formatUtcReportTimestamp,
11314
11462
  hasPluginRows,
11315
11463
  inferRuntimeLegacyHttpResponseSource,
11464
+ isDistributedReportHistoryExecution,
11316
11465
  isComparisonFailed,
11317
11466
  loadJsonObject,
11318
11467
  logLevelOrder,
@@ -0,0 +1,6 @@
1
+ import type { ReportHistoryProjection } from "./report-history.js";
2
+ export interface LocalReportInput {
3
+ history: ReportHistoryProjection;
4
+ }
5
+ export declare function emptyLocalReportInput(): LocalReportInput;
6
+ export declare function distributedLocalReportInput(): LocalReportInput;
@@ -0,0 +1,124 @@
1
+ export declare const REPORT_HISTORY_MAX_POINTS = 2048;
2
+ export declare const REPORT_HISTORY_MAX_SCALAR_VALUES = 262144;
3
+ export type ReportHistoryReasonCategory = "capture_failure" | "budget_pressure" | "distributed_temporal_aggregation_unavailable";
4
+ export interface ReportHistoryMeasurement {
5
+ count: number;
6
+ bytes: number;
7
+ approximate?: boolean;
8
+ percent50Ms?: number;
9
+ percent75Ms?: number;
10
+ percent95Ms?: number;
11
+ percent99Ms?: number;
12
+ }
13
+ export interface ReportHistoryScenario {
14
+ scenarioName: string;
15
+ sortIndex?: number;
16
+ all?: ReportHistoryMeasurement;
17
+ ok?: ReportHistoryMeasurement;
18
+ failed?: ReportHistoryMeasurement;
19
+ }
20
+ export interface ReportHistoryPoint {
21
+ elapsedSeconds: number;
22
+ terminal: boolean;
23
+ scenarios: ReportHistoryScenario[];
24
+ }
25
+ export interface AvailableReportHistoryProjection {
26
+ status: "available";
27
+ points: ReportHistoryPoint[];
28
+ }
29
+ export interface UnavailableReportHistoryProjection {
30
+ status: "unavailable";
31
+ reasonCategory: ReportHistoryReasonCategory;
32
+ points: [];
33
+ }
34
+ export type ReportHistoryProjection = AvailableReportHistoryProjection | UnavailableReportHistoryProjection;
35
+ export interface ReportHistoryRateSeries {
36
+ scenarioName: string;
37
+ values: Array<number | null>;
38
+ }
39
+ export interface ReportHistoryCollectorOptions {
40
+ scenarioCount: number;
41
+ maxPoints?: number;
42
+ nowNs?: () => bigint;
43
+ onCaptureError?: (error: unknown) => void;
44
+ }
45
+ /**
46
+ * Bounded report-only cumulative telemetry. This type is intentionally not
47
+ * exported from the package entry point and never participates in public run,
48
+ * sink, observation, or cluster payloads.
49
+ */
50
+ export declare class ReportHistoryCollector {
51
+ readonly pointLimit: number;
52
+ private readonly nowNs;
53
+ private readonly onCaptureError?;
54
+ private points;
55
+ private startNs;
56
+ private terminalCaptured;
57
+ private reasonCategory;
58
+ constructor(options: ReportHistoryCollectorOptions);
59
+ get available(): boolean;
60
+ start(startNs?: bigint): void;
61
+ capture(snapshotFactory: () => readonly ReportHistoryScenario[]): boolean;
62
+ finalize(snapshotFactory: () => readonly ReportHistoryScenario[]): boolean;
63
+ disable(reasonCategory: ReportHistoryReasonCategory): void;
64
+ toProjection(): ReportHistoryProjection;
65
+ private captureSafely;
66
+ private compact;
67
+ }
68
+ export interface ReportHistoryWorkerOptions {
69
+ collector: ReportHistoryCollector;
70
+ cadenceSeconds: number;
71
+ snapshotFactory: () => readonly ReportHistoryScenario[];
72
+ nowNs?: () => bigint;
73
+ onFailure?: (category: ReportHistoryReasonCategory, exceptionClasses: string) => void;
74
+ }
75
+ interface ReportHistoryLifecycleWorker {
76
+ start(): void;
77
+ stopAndFinalize(): void;
78
+ stopWithoutFinalizing(): void;
79
+ }
80
+ /**
81
+ * Coordinates one private history worker across every local scenario. The
82
+ * first measured phase establishes the history clock, and the last scenario
83
+ * leaving execution records the terminal point before scenario cleanup.
84
+ */
85
+ export declare class ReportHistoryLifecycleCoordinator {
86
+ private readonly worker;
87
+ private readonly scenarioCount;
88
+ private readonly terminalBoundary;
89
+ private releaseTerminalBoundary;
90
+ private measuredLoadStarted;
91
+ private endedScenarios;
92
+ private completed;
93
+ private terminalBoundaryReleased;
94
+ constructor(worker: ReportHistoryLifecycleWorker & {
95
+ scenarioCount?: number;
96
+ });
97
+ scenarioBombingStarted(): void;
98
+ scenarioExecutionEnded(): Promise<void>;
99
+ stopWithoutFinalizing(): void;
100
+ private releaseBoundary;
101
+ }
102
+ /**
103
+ * Owns the report timer. Every cadence calculation, timeout callback, snapshot,
104
+ * and projection is exception-bounded so report history can never fail a run.
105
+ */
106
+ export declare class ReportHistoryWorker {
107
+ private readonly options;
108
+ private readonly nowNs;
109
+ private timer;
110
+ private cadenceNs;
111
+ private nextDeadlineNs;
112
+ private running;
113
+ constructor(options: ReportHistoryWorkerOptions);
114
+ get started(): boolean;
115
+ start(): void;
116
+ stopAndFinalize(): void;
117
+ stopWithoutFinalizing(): void;
118
+ private scheduleNext;
119
+ private onTimer;
120
+ private fail;
121
+ }
122
+ export declare function sanitizedExceptionClassChain(error: unknown): string;
123
+ export declare function deriveScenarioRates(points: readonly ReportHistoryPoint[]): ReportHistoryRateSeries[];
124
+ export {};
@@ -0,0 +1,2 @@
1
+ export declare const REPORT_SVG_CSS: string;
2
+ export declare const REPORT_SVG_SCRIPT: string;
@@ -1,3 +1,4 @@
1
+ import { type LocalReportInput } from "./local-report-input.js";
1
2
  type ReportRecord = Record<string, any>;
2
3
  type ReportTab = [string, string, string];
3
4
  declare function buildUngroupedCorrelationChartPayload(rows: ReportRecord[]): ReportRecord;
@@ -14,7 +15,7 @@ declare function buildDotnetMetricHtml(nodeStats: ReportRecord): string;
14
15
  declare function buildDotnetGroupedCorrelationSummaryHtml(rows: ReportRecord[], groupedChartKey: string): string;
15
16
  declare function buildDotnetUngroupedCorrelationSummaryHtml(rows: ReportRecord[], groupedChartKey: string): string;
16
17
  declare function buildDotnetPluginHints(plugin: ReportRecord): string;
17
- declare function buildDotnetHtmlTabs(nodeStats: ReportRecord): ReportTab[];
18
+ declare function buildDotnetHtmlTabs(nodeStats: ReportRecord, localReportInput?: LocalReportInput): ReportTab[];
18
19
  /**
19
20
  * Exposes the build dotnet txt report operation. Use this when interacting with the SDK through this surface.
20
21
  */
@@ -28,9 +29,11 @@ export declare function buildDotnetCsvReport(nodeStats: ReportRecord): string;
28
29
  */
29
30
  export declare function buildDotnetMarkdownReport(nodeStats: ReportRecord): string;
30
31
  /**
31
- * Exposes the build dotnet html report operation. Use this when interacting with the SDK through this surface.
32
+ * Builds the portable single-file HTML report. The optional second argument is
33
+ * an internal report-only carrier used by the native runner and is never added
34
+ * to public result, sink, observation, or cluster payloads.
32
35
  */
33
- export declare function buildDotnetHtmlReport(nodeStats: ReportRecord): string;
36
+ export declare function buildDotnetHtmlReport(nodeStats: ReportRecord, localReportInput?: LocalReportInput): string;
34
37
  export declare const __loadstrikeTestExports: {
35
38
  buildDotnetFailedResponseContent: typeof buildDotnetFailedResponseContent;
36
39
  buildDotnetFailedResponseHtml: typeof buildDotnetFailedResponseHtml;
@@ -1,6 +1,7 @@
1
1
  import { type ClusterNodeResult, type LoadEngineV2PlanInput } from "./cluster.js";
2
2
  import { RedisCorrelationStore, RedisCorrelationStoreOptions, TrackingFieldSelector, type TrackingPayload } from "./correlation.js";
3
3
  import { type EndpointAdapter, type EndpointDefinition } from "./transports.js";
4
+ import { type ReportHistoryMeasurement, type ReportHistoryScenario } from "./report-history.js";
4
5
  import { LoadEngineV2ExecutionBudget, type LoadEngineV2DistributionRecord, type LoadStrikeHistogramV1Sidecar } from "./load-engine-v2.js";
5
6
  import { type LoadStrikeIterationObservationBatchV1, type LoadStrikeIterationObservationStreamCompletionV1, type LoadStrikeIterationStepObservationV1, type LoadStrikeObservationDeliveryStats } from "./iteration-observations.js";
6
7
  declare const LOAD_ENGINE_V2_SCHEDULER_DISTRIBUTIONS: unique symbol;
@@ -570,6 +571,17 @@ declare class MeasurementAccumulator {
570
571
  */
571
572
  build(allRequestCount: number, durationMs: number): LoadStrikeMeasurementStats;
572
573
  buildCombined(other: MeasurementAccumulator, allRequestCount: number, durationMs: number): LoadStrikeMeasurementStats;
574
+ /**
575
+ * Projects only the cumulative fields used by the private HTML-report
576
+ * history. Legacy distributions deliberately omit latency so this cadence
577
+ * path never scans their retained per-request arrays.
578
+ */
579
+ buildReportHistoryMeasurement(): ReportHistoryMeasurement;
580
+ /**
581
+ * Combines only bounded native latency state plus exact counters. It does
582
+ * not materialize status rows or any public measurement DTO.
583
+ */
584
+ buildCombinedReportHistoryMeasurement(other: MeasurementAccumulator): ReportHistoryMeasurement;
573
585
  private histogramSnapshot;
574
586
  }
575
587
  declare class StepStatsAccumulator {
@@ -627,6 +639,11 @@ declare class ScenarioStatsAccumulator {
627
639
  * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
628
640
  */
629
641
  recordStep(stepName: string, reply: LoadStrikeStepReply, observedLatencyMs: number, sortIndex?: number): number;
642
+ /**
643
+ * Captures the bounded, private history view without building scenario
644
+ * steps, status rows, plugins, aliases, or any other public result shape.
645
+ */
646
+ buildReportHistorySnapshot(): ReportHistoryScenario;
630
647
  /**
631
648
  * Builds the configured payload or helper object.
632
649
  * Use this when all builder inputs are ready to be materialized.
@@ -2384,6 +2401,8 @@ export declare class LoadStrikeRunner {
2384
2401
  private collectPluginData;
2385
2402
  }
2386
2403
  declare function hasPluginRows(value: LoadStrikePluginData): boolean;
2404
+ declare function boundedReportingIntervalMs(seconds: number): number;
2405
+ declare function resolvedReportHistoryCadenceSeconds(seconds: number): number;
2387
2406
  declare function combineAbortSignals(...signals: AbortSignal[]): AbortSignal;
2388
2407
  declare function delayWithAbort(durationMs: number, signal: AbortSignal): Promise<void>;
2389
2408
  declare function createRuntimeRandom(): LoadStrikeRandom;
@@ -2434,6 +2453,7 @@ declare function normalizeThresholdScope(value: string): "scenario" | "step" | "
2434
2453
  declare function buildThresholdCheckExpression(scope: "scenario" | "step" | "metric", stepName: string, field: string): string;
2435
2454
  declare function detailedToNodeStats(result: LoadStrikeRunResult, metricStats?: LoadStrikeMetricStats): LoadStrikeNodeStats;
2436
2455
  declare function resolveClusterExecutionMode(options: LoadStrikeRunnerOptions): "single" | "local-coordinator" | "nats-coordinator" | "nats-agent";
2456
+ declare function isDistributedReportHistoryExecution(clusterMode: ReturnType<typeof resolveClusterExecutionMode>, options: Pick<LoadStrikeRunnerOptions, "clusterShardCount" | "loadEngineV2SegmentLifecycleOverride">): boolean;
2437
2457
  declare function buildEmptyNodeStats(args: {
2438
2458
  startedUtc: string;
2439
2459
  completedUtc: string;
@@ -2644,6 +2664,8 @@ export declare const __loadstrikeTestExports: {
2644
2664
  buildThresholdCheckExpression: typeof buildThresholdCheckExpression;
2645
2665
  buildTrackingLeaseKey: typeof buildTrackingLeaseKey;
2646
2666
  buildTrackingRunNamespace: typeof buildTrackingRunNamespace;
2667
+ boundedReportingIntervalMs: typeof boundedReportingIntervalMs;
2668
+ resolvedReportHistoryCadenceSeconds: typeof resolvedReportHistoryCadenceSeconds;
2647
2669
  clusterNodeResultToNodeStats: typeof clusterNodeResultToNodeStats;
2648
2670
  combineAbortSignals: typeof combineAbortSignals;
2649
2671
  computeScenarioRequestCount: typeof computeScenarioRequestCount;
@@ -2663,6 +2685,7 @@ export declare const __loadstrikeTestExports: {
2663
2685
  formatUtcReportTimestamp: typeof formatUtcReportTimestamp;
2664
2686
  hasPluginRows: typeof hasPluginRows;
2665
2687
  inferRuntimeLegacyHttpResponseSource: typeof inferRuntimeLegacyHttpResponseSource;
2688
+ isDistributedReportHistoryExecution: typeof isDistributedReportHistoryExecution;
2666
2689
  isComparisonFailed: typeof isComparisonFailed;
2667
2690
  loadJsonObject: typeof loadJsonObject;
2668
2691
  logLevelOrder: typeof logLevelOrder;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loadstrike/loadstrike-sdk",
3
- "version": "1.0.31001",
3
+ "version": "1.0.31601",
4
4
  "description": "TypeScript and JavaScript SDK for in-process load execution, traffic correlation, and reporting.",
5
5
  "keywords": [
6
6
  "load-testing",