@loadstrike/loadstrike-sdk 1.0.30201 → 1.0.31001

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cjs/local.js CHANGED
@@ -46,7 +46,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
46
46
  var __importDefault = (this && this.__importDefault) || function (mod) {
47
47
  return (mod && mod.__esModule) ? mod : { "default": mod };
48
48
  };
49
- var _LoadStrikeLocalClient_licensingApiBaseUrl, _LoadStrikeLocalClient_signingKeyCache;
49
+ var _LoadStrikeLocalClient_licensingApiBaseUrl, _LoadStrikeLocalClient_signingKeyCache, _LoadStrikeLocalClient_heartbeatDrains;
50
50
  Object.defineProperty(exports, "__esModule", { value: true });
51
51
  exports.__loadstrikeTestExports = exports.LoadStrikeLocalClient = void 0;
52
52
  const node_os_1 = __importDefault(require("node:os"));
@@ -109,6 +109,7 @@ class LoadStrikeLocalClient {
109
109
  constructor(options = {}) {
110
110
  _LoadStrikeLocalClient_licensingApiBaseUrl.set(this, void 0);
111
111
  _LoadStrikeLocalClient_signingKeyCache.set(this, new Map());
112
+ _LoadStrikeLocalClient_heartbeatDrains.set(this, new WeakMap());
112
113
  assertNoDisableLicenseEnforcementOption(options, "LoadStrikeLocalClient");
113
114
  __classPrivateFieldSet(this, _LoadStrikeLocalClient_licensingApiBaseUrl, resolveLicensingApiBaseUrl(), "f");
114
115
  this.licenseValidationTimeoutMs = normalizeTimeoutMs(options.licenseValidationTimeoutMs);
@@ -228,28 +229,59 @@ class LoadStrikeLocalClient {
228
229
  }
229
230
  await this.verifySignedRunToken(runToken, request, requestedFeatures, runnerKey, sessionId, computedDeviceHash);
230
231
  const heartbeatIntervalSeconds = Math.max(asInt(pickValue(json, "HeartbeatIntervalSeconds", "heartbeatIntervalSeconds")), 1);
231
- const heartbeatTimer = setInterval(() => {
232
- void this.sendRunTokenHeartbeat({
233
- runToken,
234
- sessionId,
235
- deviceHash: computedDeviceHash,
236
- machineName,
237
- environmentClassification
238
- }).catch(() => {
239
- // Best-effort heartbeat: server-side lease expiration is authoritative.
240
- });
241
- }, heartbeatIntervalSeconds * 1000);
242
- if (typeof heartbeatTimer.unref === "function") {
243
- heartbeatTimer.unref();
244
- }
245
- return {
232
+ const session = {
246
233
  runToken,
247
234
  sessionId,
248
235
  deviceHash: computedDeviceHash,
249
236
  machineName,
250
- environmentClassification,
251
- heartbeatTimer
237
+ environmentClassification
252
238
  };
239
+ let heartbeatInFlight = false;
240
+ const runHeartbeat = async () => {
241
+ heartbeatInFlight = true;
242
+ try {
243
+ const currentRunToken = stringOrDefault(session.runToken, "").trim();
244
+ if (!currentRunToken) {
245
+ return;
246
+ }
247
+ const refreshedRunToken = await this.sendRunTokenHeartbeat({
248
+ runToken: currentRunToken,
249
+ sessionId,
250
+ deviceHash: computedDeviceHash,
251
+ machineName,
252
+ environmentClassification
253
+ });
254
+ if (!refreshedRunToken) {
255
+ return;
256
+ }
257
+ try {
258
+ await this.verifySignedRunToken(refreshedRunToken, request, requestedFeatures, runnerKey, sessionId, computedDeviceHash);
259
+ }
260
+ catch {
261
+ return;
262
+ }
263
+ session.runToken = refreshedRunToken;
264
+ }
265
+ catch {
266
+ // Best-effort heartbeat: server-side lease expiration is authoritative.
267
+ }
268
+ finally {
269
+ heartbeatInFlight = false;
270
+ }
271
+ };
272
+ let currentHeartbeat = Promise.resolve();
273
+ const heartbeatTimer = setInterval(() => {
274
+ if (!heartbeatInFlight) {
275
+ currentHeartbeat = runHeartbeat();
276
+ }
277
+ return currentHeartbeat;
278
+ }, heartbeatIntervalSeconds * 1000);
279
+ if (typeof heartbeatTimer.unref === "function") {
280
+ heartbeatTimer.unref();
281
+ }
282
+ session.heartbeatTimer = heartbeatTimer;
283
+ __classPrivateFieldGet(this, _LoadStrikeLocalClient_heartbeatDrains, "f").set(session, () => currentHeartbeat);
284
+ return session;
253
285
  }
254
286
  finally {
255
287
  clearTimeout(timer);
@@ -433,18 +465,35 @@ class LoadStrikeLocalClient {
433
465
  const timer = controller
434
466
  ? setTimeout(() => controller.abort(), this.licenseValidationTimeoutMs)
435
467
  : null;
436
- const { response } = await this.postLicensingRequest("/api/v1/licenses/heartbeat", heartbeatPayload, signal ?? controller.signal);
468
+ const { response, json } = await this.postLicensingRequest("/api/v1/licenses/heartbeat", heartbeatPayload, signal ?? controller.signal);
437
469
  if (timer) {
438
470
  clearTimeout(timer);
439
471
  }
440
472
  if (!response.ok) {
441
473
  throw new Error(`Runner key validation denied. DenialCode=run_token_heartbeat_failed, Message=Run token heartbeat failed with status ${response.status}.`);
442
474
  }
475
+ if (pickValue(json ?? {}, "IsValid", "isValid") !== true) {
476
+ return undefined;
477
+ }
478
+ const refreshedRunToken = stringOrDefault(pickValue(json ?? {}, "RunToken", "runToken"), "").trim();
479
+ return refreshedRunToken || undefined;
443
480
  }
444
481
  async stopLicenseLeaseIfRequired(session, _request) {
445
482
  if (session.heartbeatTimer) {
446
483
  clearInterval(session.heartbeatTimer);
447
484
  }
485
+ const heartbeatDrain = __classPrivateFieldGet(this, _LoadStrikeLocalClient_heartbeatDrains, "f").get(session);
486
+ if (heartbeatDrain) {
487
+ try {
488
+ await heartbeatDrain();
489
+ }
490
+ catch {
491
+ // Heartbeats are best-effort and retain the last fully verified token.
492
+ }
493
+ finally {
494
+ __classPrivateFieldGet(this, _LoadStrikeLocalClient_heartbeatDrains, "f").delete(session);
495
+ }
496
+ }
448
497
  if (!session.runToken) {
449
498
  return;
450
499
  }
@@ -503,7 +552,7 @@ class LoadStrikeLocalClient {
503
552
  }
504
553
  }
505
554
  exports.LoadStrikeLocalClient = LoadStrikeLocalClient;
506
- _LoadStrikeLocalClient_licensingApiBaseUrl = new WeakMap(), _LoadStrikeLocalClient_signingKeyCache = new WeakMap();
555
+ _LoadStrikeLocalClient_licensingApiBaseUrl = new WeakMap(), _LoadStrikeLocalClient_signingKeyCache = new WeakMap(), _LoadStrikeLocalClient_heartbeatDrains = new WeakMap();
507
556
  function assertNoDisableLicenseEnforcementOption(value, source) {
508
557
  if (value == null || typeof value !== "object" || Array.isArray(value)) {
509
558
  return;
@@ -1369,9 +1418,20 @@ function readTrackingId(payload, selector) {
1369
1418
  return null;
1370
1419
  }
1371
1420
  let current = body;
1372
- for (const segment of path.split(".").filter(Boolean)) {
1421
+ let segments;
1422
+ try {
1423
+ segments = safeJsonPathSegments(path);
1424
+ }
1425
+ catch {
1426
+ return null;
1427
+ }
1428
+ for (const segment of segments) {
1373
1429
  if (current && typeof current === "object" && !Array.isArray(current)) {
1374
- current = current[segment];
1430
+ const record = current;
1431
+ if (!Object.prototype.hasOwnProperty.call(record, segment)) {
1432
+ return null;
1433
+ }
1434
+ current = record[segment];
1375
1435
  }
1376
1436
  else {
1377
1437
  return null;
@@ -1404,23 +1464,57 @@ function readOptionalTrackingSelectorValue(value) {
1404
1464
  return undefined;
1405
1465
  }
1406
1466
  function setJsonPathValue(body, path, value) {
1407
- const target = body && typeof body === "object" && !Array.isArray(body)
1408
- ? { ...body }
1409
- : {};
1410
- const segments = path.split(".").filter(Boolean);
1467
+ const target = cloneJsonRecord(body);
1468
+ const segments = safeJsonPathSegments(path);
1411
1469
  if (!segments.length) {
1412
1470
  return target;
1413
1471
  }
1414
1472
  let current = target;
1415
1473
  for (let i = 0; i < segments.length - 1; i += 1) {
1416
1474
  const segment = segments[i];
1417
- const next = current[segment];
1475
+ const next = readOwnJsonProperty(current, segment);
1476
+ let child;
1418
1477
  if (!next || typeof next !== "object" || Array.isArray(next)) {
1419
- current[segment] = {};
1478
+ child = {};
1420
1479
  }
1421
- current = current[segment];
1480
+ else {
1481
+ child = cloneJsonRecord(next);
1482
+ }
1483
+ defineJsonProperty(current, segment, child);
1484
+ current = child;
1485
+ }
1486
+ defineJsonProperty(current, segments[segments.length - 1], value);
1487
+ return target;
1488
+ }
1489
+ const FORBIDDEN_JSON_PATH_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
1490
+ function safeJsonPathSegments(path) {
1491
+ const segments = path.split(".").filter(Boolean);
1492
+ const forbidden = segments.find((segment) => FORBIDDEN_JSON_PATH_SEGMENTS.has(segment));
1493
+ if (forbidden) {
1494
+ throw new Error(`Tracking selector contains forbidden JSON path segment '${forbidden}'.`);
1495
+ }
1496
+ return segments;
1497
+ }
1498
+ function defineJsonProperty(target, key, value) {
1499
+ Object.defineProperty(target, key, {
1500
+ configurable: true,
1501
+ enumerable: true,
1502
+ value,
1503
+ writable: true
1504
+ });
1505
+ }
1506
+ function readOwnJsonProperty(target, key) {
1507
+ const descriptor = Object.getOwnPropertyDescriptor(target, key);
1508
+ return descriptor && "value" in descriptor ? descriptor.value : undefined;
1509
+ }
1510
+ function cloneJsonRecord(value) {
1511
+ const target = {};
1512
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1513
+ return target;
1514
+ }
1515
+ for (const [key, entry] of Object.entries(value)) {
1516
+ defineJsonProperty(target, key, entry);
1422
1517
  }
1423
- current[segments[segments.length - 1]] = value;
1424
1518
  return target;
1425
1519
  }
1426
1520
  function mapCorrelationStore(tracking, runNamespace) {
@@ -1867,6 +1961,10 @@ function isRuntimeReportingSink(value) {
1867
1961
  "SaveRealtimeMetrics",
1868
1962
  "saveRunResult",
1869
1963
  "SaveRunResult",
1964
+ "saveIterationBatch",
1965
+ "SaveIterationBatch",
1966
+ "completeIterationObservationStream",
1967
+ "CompleteIterationObservationStream",
1870
1968
  "stop",
1871
1969
  "Stop"
1872
1970
  ].some((name) => typeof record[name] === "function");
@@ -45,8 +45,12 @@ function reportValue(source, ...keys) {
45
45
  }
46
46
  const record = source;
47
47
  for (const key of keys) {
48
- if (record[key] !== undefined && record[key] !== null) {
49
- return record[key];
48
+ const descriptor = Object.getOwnPropertyDescriptor(record, key);
49
+ if (descriptor
50
+ && "value" in descriptor
51
+ && descriptor.value !== undefined
52
+ && descriptor.value !== null) {
53
+ return descriptor.value;
50
54
  }
51
55
  }
52
56
  return undefined;
@@ -139,6 +143,26 @@ function asFloat(value) {
139
143
  const parsed = Number.parseFloat(asString(value));
140
144
  return Number.isFinite(parsed) ? parsed : 0;
141
145
  }
146
+ function combinedMeasurement(source) {
147
+ const all = reportValue(source, "allMeasurement", "AllMeasurement");
148
+ if (all && typeof all === "object" && !Array.isArray(all)) {
149
+ return all;
150
+ }
151
+ const ok = reportObject(source, "ok", "Ok");
152
+ const fail = reportObject(source, "fail", "Fail");
153
+ const okCount = asInt(reportValue(reportObject(ok, "request", "Request"), "count", "Count"));
154
+ const failCount = asInt(reportValue(reportObject(fail, "request", "Request"), "count", "Count"));
155
+ if (okCount === 0) {
156
+ return fail;
157
+ }
158
+ return failCount === 0 ? ok : undefined;
159
+ }
160
+ function formatCombinedLatency(source, ...keys) {
161
+ const measurement = combinedMeasurement(source);
162
+ return measurement
163
+ ? formatReportNumber(reportValue(reportObject(measurement, "latency", "Latency"), ...keys))
164
+ : "n/a";
165
+ }
142
166
  function asBool(value) {
143
167
  if (typeof value === "boolean") {
144
168
  return value;
@@ -309,7 +333,22 @@ function formatDotnetDateTime(value) {
309
333
  return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${fraction}Z`;
310
334
  }
311
335
  function escapeJsonForHtmlScript(value) {
312
- return value.split("</").join("<\\/").split("<!--").join("<\\!--");
336
+ return value.replace(/[<>&\u2028\u2029]/g, (character) => {
337
+ switch (character) {
338
+ case "<":
339
+ return "\\u003c";
340
+ case ">":
341
+ return "\\u003e";
342
+ case "&":
343
+ return "\\u0026";
344
+ case "\u2028":
345
+ return "\\u2028";
346
+ case "\u2029":
347
+ return "\\u2029";
348
+ default:
349
+ return character;
350
+ }
351
+ });
313
352
  }
314
353
  function buildReportLogoDataUri(resourceName) {
315
354
  const cached = REPORT_LOGO_CACHE.get(resourceName);
@@ -754,8 +793,8 @@ function buildDotnetScenarioRows(nodeStats) {
754
793
  FAIL: reportFailCountValue(scenario),
755
794
  Duration: formatDotnetTimeSpan(reportDurationValue(scenario)),
756
795
  RPS: formatReportNumber(reportDurationSeconds(reportDurationValue(scenario)) <= 0 ? 0 : requestCount / reportDurationSeconds(reportDurationValue(scenario))),
757
- LatencyP95Ms: formatReportNumber(Math.max(asFloat(reportValue(reportObject(reportObject(scenario, "ok", "Ok"), "latency", "Latency"), "percent95", "Percent95")), asFloat(reportValue(reportObject(reportObject(scenario, "fail", "Fail"), "latency", "Latency"), "percent95", "Percent95")))),
758
- LatencyP99Ms: formatReportNumber(Math.max(asFloat(reportValue(reportObject(reportObject(scenario, "ok", "Ok"), "latency", "Latency"), "percent99", "Percent99")), asFloat(reportValue(reportObject(reportObject(scenario, "fail", "Fail"), "latency", "Latency"), "percent99", "Percent99")))),
796
+ LatencyP95Ms: formatCombinedLatency(scenario, "percent95", "Percent95"),
797
+ LatencyP99Ms: formatCombinedLatency(scenario, "percent99", "Percent99"),
759
798
  CurrentOperation: asString(reportValue(scenario, "currentOperation", "CurrentOperation"))
760
799
  };
761
800
  });
@@ -772,8 +811,8 @@ function buildDotnetStepRows(nodeStats) {
772
811
  FAIL: asInt(reportValue(reportObject(reportObject(step, "fail", "Fail"), "request", "Request"), "count", "Count")),
773
812
  OK_RPS: formatReportNumber(reportValue(reportObject(reportObject(step, "ok", "Ok"), "request", "Request"), "rps", "RPS")),
774
813
  FAIL_RPS: formatReportNumber(reportValue(reportObject(reportObject(step, "fail", "Fail"), "request", "Request"), "rps", "RPS")),
775
- LatencyMeanMs: formatReportNumber(Math.max(asFloat(reportValue(reportObject(reportObject(step, "ok", "Ok"), "latency", "Latency"), "meanMs", "MeanMs")), asFloat(reportValue(reportObject(reportObject(step, "fail", "Fail"), "latency", "Latency"), "meanMs", "MeanMs")))),
776
- P95LatencyMs: formatReportNumber(Math.max(asFloat(reportValue(reportObject(reportObject(step, "ok", "Ok"), "latency", "Latency"), "percent95", "Percent95")), asFloat(reportValue(reportObject(reportObject(step, "fail", "Fail"), "latency", "Latency"), "percent95", "Percent95"))))
814
+ LatencyMeanMs: formatCombinedLatency(step, "meanMs", "MeanMs"),
815
+ P95LatencyMs: formatCombinedLatency(step, "percent95", "Percent95")
777
816
  });
778
817
  }
779
818
  }
@@ -783,6 +822,10 @@ function buildDotnetScenarioMeasurementRows(nodeStats) {
783
822
  const rows = [];
784
823
  for (const scenario of reportScenarios(nodeStats)) {
785
824
  const scenarioName = asString(reportValue(scenario, "scenarioName", "ScenarioName"));
825
+ const allMeasurement = reportValue(scenario, "allMeasurement", "AllMeasurement");
826
+ if (allMeasurement && typeof allMeasurement === "object" && !Array.isArray(allMeasurement)) {
827
+ pushMeasurementRowIfData(rows, "Scenario", scenarioName, "", "ALL", allMeasurement);
828
+ }
786
829
  pushMeasurementRowIfData(rows, "Scenario", scenarioName, "", "OK", reportObject(scenario, "ok", "Ok"));
787
830
  pushMeasurementRowIfData(rows, "Scenario", scenarioName, "", "FAIL", reportObject(scenario, "fail", "Fail"));
788
831
  }
@@ -794,6 +837,10 @@ function buildDotnetStepMeasurementRows(nodeStats) {
794
837
  const scenarioName = asString(reportValue(scenario, "scenarioName", "ScenarioName"));
795
838
  for (const step of reportSteps(scenario)) {
796
839
  const stepName = asString(reportValue(step, "stepName", "StepName"));
840
+ const allMeasurement = reportValue(step, "allMeasurement", "AllMeasurement");
841
+ if (allMeasurement && typeof allMeasurement === "object" && !Array.isArray(allMeasurement)) {
842
+ pushMeasurementRowIfData(rows, "Step", scenarioName, stepName, "ALL", allMeasurement);
843
+ }
797
844
  pushMeasurementRowIfData(rows, "Step", scenarioName, stepName, "OK", reportObject(step, "ok", "Ok"));
798
845
  pushMeasurementRowIfData(rows, "Step", scenarioName, stepName, "FAIL", reportObject(step, "fail", "Fail"));
799
846
  }
@@ -874,6 +921,9 @@ function buildDotnetStatusCodeClassChart(scenarios) {
874
921
  }
875
922
  function buildDotnetChartData(nodeStats) {
876
923
  const scenarios = reportScenarios(nodeStats);
924
+ const combinedScenarios = scenarios
925
+ .map((scenario) => ({ scenario, measurement: combinedMeasurement(scenario) }))
926
+ .filter((item) => item.measurement !== undefined);
877
927
  return {
878
928
  overallOutcome: [
879
929
  { label: "OK", value: reportTotalOkCount(nodeStats, scenarios), color: "#18a957" },
@@ -884,9 +934,9 @@ function buildDotnetChartData(nodeStats) {
884
934
  value: reportRequestCountValue(scenario),
885
935
  color: "#3b82f6"
886
936
  })),
887
- scenarioP95Latency: scenarios.map((scenario) => ({
937
+ scenarioP95Latency: combinedScenarios.map(({ scenario, measurement }) => ({
888
938
  label: asString(reportValue(scenario, "scenarioName", "ScenarioName")),
889
- value: Math.max(asFloat(reportValue(reportObject(reportObject(scenario, "ok", "Ok"), "latency", "Latency"), "percent95", "Percent95")), asFloat(reportValue(reportObject(reportObject(scenario, "fail", "Fail"), "latency", "Latency"), "percent95", "Percent95"))),
939
+ value: asFloat(reportValue(reportObject(measurement, "latency", "Latency"), "percent95", "Percent95")),
890
940
  color: "#8b5cf6"
891
941
  })),
892
942
  scenarioRps: scenarios.map((scenario) => ({
@@ -910,12 +960,12 @@ function buildDotnetChartData(nodeStats) {
910
960
  })),
911
961
  statusCodeClasses: buildDotnetStatusCodeClassChart(scenarios),
912
962
  scenarioLatencyTrend: {
913
- labels: scenarios.map((scenario) => asString(reportValue(scenario, "scenarioName", "ScenarioName"))),
963
+ labels: combinedScenarios.map(({ scenario }) => asString(reportValue(scenario, "scenarioName", "ScenarioName"))),
914
964
  series: [
915
- { name: "P50", color: "#38bdf8", values: scenarios.map((scenario) => Math.max(asFloat(reportValue(reportObject(reportObject(scenario, "ok", "Ok"), "latency", "Latency"), "percent50", "Percent50")), asFloat(reportValue(reportObject(reportObject(scenario, "fail", "Fail"), "latency", "Latency"), "percent50", "Percent50")))) },
916
- { name: "P75", color: "#22c55e", values: scenarios.map((scenario) => Math.max(asFloat(reportValue(reportObject(reportObject(scenario, "ok", "Ok"), "latency", "Latency"), "percent75", "Percent75")), asFloat(reportValue(reportObject(reportObject(scenario, "fail", "Fail"), "latency", "Latency"), "percent75", "Percent75")))) },
917
- { name: "P95", color: "#f59e0b", values: scenarios.map((scenario) => Math.max(asFloat(reportValue(reportObject(reportObject(scenario, "ok", "Ok"), "latency", "Latency"), "percent95", "Percent95")), asFloat(reportValue(reportObject(reportObject(scenario, "fail", "Fail"), "latency", "Latency"), "percent95", "Percent95")))) },
918
- { name: "P99", color: "#f43f5e", values: scenarios.map((scenario) => Math.max(asFloat(reportValue(reportObject(reportObject(scenario, "ok", "Ok"), "latency", "Latency"), "percent99", "Percent99")), asFloat(reportValue(reportObject(reportObject(scenario, "fail", "Fail"), "latency", "Latency"), "percent99", "Percent99")))) }
965
+ { name: "P50", color: "#38bdf8", values: combinedScenarios.map(({ measurement }) => asFloat(reportValue(reportObject(measurement, "latency", "Latency"), "percent50", "Percent50"))) },
966
+ { name: "P75", color: "#22c55e", values: combinedScenarios.map(({ measurement }) => asFloat(reportValue(reportObject(measurement, "latency", "Latency"), "percent75", "Percent75"))) },
967
+ { name: "P95", color: "#f59e0b", values: combinedScenarios.map(({ measurement }) => asFloat(reportValue(reportObject(measurement, "latency", "Latency"), "percent95", "Percent95"))) },
968
+ { name: "P99", color: "#f43f5e", values: combinedScenarios.map(({ measurement }) => asFloat(reportValue(reportObject(measurement, "latency", "Latency"), "percent99", "Percent99"))) }
919
969
  ]
920
970
  }
921
971
  };
@@ -1060,6 +1110,82 @@ function buildDotnetThresholdHtml(nodeStats) {
1060
1110
  function buildDotnetMetricHtml(nodeStats) {
1061
1111
  return buildDotnetTableHtml(buildDotnetMetricRows(nodeStats));
1062
1112
  }
1113
+ function buildDotnetGeneratorDeliveryHtml(nodeStats) {
1114
+ const warnings = reportArray(nodeStats, "generatorWarnings", "GeneratorWarnings");
1115
+ const stats = reportObject(nodeStats, "schedulerStats", "SchedulerStats");
1116
+ const topLevelSegments = reportArray(nodeStats, "schedulerSegments", "SchedulerSegments");
1117
+ const segments = topLevelSegments.length > 0
1118
+ ? topLevelSegments
1119
+ : reportArray(stats, "segments", "Segments");
1120
+ const observationStats = reportObject(nodeStats, "observationDeliveryStats", "ObservationDeliveryStats");
1121
+ const reportingCompleteValue = reportValue(nodeStats, "reportingComplete", "ReportingComplete");
1122
+ const reportingComplete = reportingCompleteValue == null
1123
+ ? "N/A"
1124
+ : asBool(reportingCompleteValue) ? "Yes" : "No";
1125
+ const parts = [];
1126
+ appendReportLine(parts, "<div class=\"card\">");
1127
+ appendReportLine(parts, "<h2>Generator Delivery</h2>");
1128
+ appendReportLine(parts, "<p>Application failures remain separate from generator and reporting warnings. Dropped arrivals, unavailable worker slots, and raw observation loss are not synthetic SUT errors.</p>");
1129
+ appendReportLine(parts, "<div class=\"card-grid\">");
1130
+ for (const [label, value] of [
1131
+ ["Configured Max In Flight", asInt(reportValue(stats, "configuredMaxInFlight", "ConfiguredMaxInFlight"))],
1132
+ ["Observed Max In Flight", asInt(reportValue(stats, "maxInFlightObserved", "MaxInFlightObserved"))],
1133
+ ["Current In Flight", asInt(reportValue(stats, "currentInFlight", "CurrentInFlight"))],
1134
+ ["Observation Captured", asString(reportValue(observationStats, "capturedCount64", "CapturedCount64")) || "0"],
1135
+ ["Observation Delivered", asString(reportValue(observationStats, "deliveredCount64", "DeliveredCount64")) || "0"],
1136
+ ["Observation Buffer Drops", asString(reportValue(observationStats, "droppedBufferCount64", "DroppedBufferCount64")) || "0"],
1137
+ ["Observation Sink Drops", asString(reportValue(observationStats, "droppedSinkCount64", "DroppedSinkCount64")) || "0"],
1138
+ ["Reporting Complete", reportingComplete],
1139
+ ["Warning Groups", warnings.length]
1140
+ ]) {
1141
+ appendReportLine(parts, `<div class="stat-card"><div class="stat-label">${escapeHtml(label)}</div><div class="stat-value">${escapeHtml(value)}</div></div>`);
1142
+ }
1143
+ appendReportLine(parts, "</div></div>");
1144
+ if (warnings.length) {
1145
+ appendReportLine(parts, "<div class=\"card\"><h2>Generator Warnings</h2>");
1146
+ parts.push(buildDotnetTableHtml(warnings, false));
1147
+ appendReportLine(parts, "</div>");
1148
+ }
1149
+ if (segments.length) {
1150
+ const rows = segments.map((segment) => ({
1151
+ Scenario: asString(reportValue(segment, "scenarioName", "ScenarioName")),
1152
+ Simulation: asString(reportValue(segment, "kind", "Kind")),
1153
+ Shard: `${asInt(reportValue(segment, "shardIndex", "ShardIndex"))}/${Math.max(asInt(reportValue(segment, "shardCount", "ShardCount")), 1)}`,
1154
+ Planned: asString(reportValue(segment, "plannedIterations64", "PlannedIterations64")),
1155
+ Due: asString(reportValue(segment, "dueIterations64", "DueIterations64")),
1156
+ Started: asString(reportValue(segment, "startedIterations64", "StartedIterations64")),
1157
+ Completed: asString(reportValue(segment, "completedIterations64", "CompletedIterations64")),
1158
+ Dropped: asString(reportValue(segment, "droppedIterations64", "DroppedIterations64")),
1159
+ Unreached: asString(reportValue(segment, "unreachedIterations64", "UnreachedIterations64")),
1160
+ "Unavailable Workers": asString(reportValue(segment, "unavailableWorkerSlots64", "UnavailableWorkerSlots64")),
1161
+ "Delivery %": formatReportNumber(reportValue(segment, "deliveryPercent", "DeliveryPercent")),
1162
+ "Accounting Complete": Boolean(reportValue(segment, "accountingComplete", "AccountingComplete"))
1163
+ }));
1164
+ appendReportLine(parts, "<div class=\"card\"><h2>Scheduler Segments</h2>");
1165
+ parts.push(buildDotnetTableHtml(rows, false));
1166
+ appendReportLine(parts, "</div>");
1167
+ }
1168
+ return parts.join("");
1169
+ }
1170
+ function hasDotnetGeneratorDeliveryData(nodeStats) {
1171
+ const schedulerStats = reportObject(nodeStats, "schedulerStats", "SchedulerStats");
1172
+ const observationStats = reportObject(nodeStats, "observationDeliveryStats", "ObservationDeliveryStats");
1173
+ const hasNonZeroDecimal = (value) => {
1174
+ const text = asString(value);
1175
+ return text.trim().length > 0 && text !== "0";
1176
+ };
1177
+ return reportArray(nodeStats, "generatorWarnings", "GeneratorWarnings").length > 0
1178
+ || reportArray(nodeStats, "schedulerSegments", "SchedulerSegments").length > 0
1179
+ || reportArray(schedulerStats, "segments", "Segments").length > 0
1180
+ || asInt(reportValue(schedulerStats, "configuredMaxInFlight", "ConfiguredMaxInFlight")) > 0
1181
+ || asInt(reportValue(schedulerStats, "maxInFlightObserved", "MaxInFlightObserved")) > 0
1182
+ || asInt(reportValue(schedulerStats, "currentInFlight", "CurrentInFlight")) > 0
1183
+ || reportValue(nodeStats, "reportingComplete", "ReportingComplete") === false
1184
+ || hasNonZeroDecimal(reportValue(observationStats, "capturedCount64", "CapturedCount64"))
1185
+ || hasNonZeroDecimal(reportValue(observationStats, "deliveredCount64", "DeliveredCount64"))
1186
+ || hasNonZeroDecimal(reportValue(observationStats, "droppedBufferCount64", "DroppedBufferCount64"))
1187
+ || hasNonZeroDecimal(reportValue(observationStats, "droppedSinkCount64", "DroppedSinkCount64"));
1188
+ }
1063
1189
  function buildDotnetGroupedCorrelationSummaryHtml(rows, groupedChartKey) {
1064
1190
  const parts = [];
1065
1191
  const payloads = buildGroupedCorrelationChartPayloads(rows);
@@ -1136,6 +1262,9 @@ function buildDotnetHtmlTabs(nodeStats) {
1136
1262
  if (metricRows.length) {
1137
1263
  tabs.push(["metrics", "Metrics", buildDotnetTableHtml(metricRows)]);
1138
1264
  }
1265
+ if (hasDotnetGeneratorDeliveryData(nodeStats)) {
1266
+ tabs.push(["generator-delivery", "Generator Delivery", buildDotnetGeneratorDeliveryHtml(nodeStats)]);
1267
+ }
1139
1268
  for (const plugin of reportArray(nodeStats, "pluginsData", "PluginsData")) {
1140
1269
  const pluginName = asString(reportValue(plugin, "pluginName", "PluginName"));
1141
1270
  const hints = buildDotnetPluginHints(plugin);
@@ -1169,7 +1298,7 @@ function buildDotnetHtmlTabs(nodeStats) {
1169
1298
  body = buildDotnetGroupedCorrelationSummaryHtml(bodyRows, `grouped-correlation-${tabs.length}`);
1170
1299
  }
1171
1300
  else if (lowerPlugin.includes("correlation") && lowerTable.includes("ungrouped correlation rows")) {
1172
- title = "Ungrouped Corelation Summary";
1301
+ title = "Ungrouped Correlation Summary";
1173
1302
  body = buildDotnetUngroupedCorrelationSummaryHtml(bodyRows, `ungrouped-correlation-${tabs.length}`);
1174
1303
  }
1175
1304
  tabs.push([`plugin-${tabs.length}`, title, `${hints}${body}`]);
@@ -1285,9 +1414,9 @@ function buildDotnetMarkdownReport(nodeStats) {
1285
1414
  */
1286
1415
  function buildDotnetHtmlReport(nodeStats) {
1287
1416
  const tabs = buildDotnetHtmlTabs(nodeStats);
1288
- const buttonsHtml = tabs.map(([tabId, title]) => `<button class="tab-btn" data-tab="${tabId}">${escapeHtml(title)}</button>${REPORT_EOL}`).join("");
1289
- const sectionsHtml = tabs.map(([tabId, , html]) => `<section id="${tabId}" class="tab">${html}</section>${REPORT_EOL}`).join("");
1290
- const chartDataJson = JSON.stringify(buildDotnetChartData(nodeStats));
1417
+ const buttonsHtml = tabs.map(([tabId, title]) => `<button class="tab-btn" data-tab="${escapeHtml(tabId)}">${escapeHtml(title)}</button>${REPORT_EOL}`).join("");
1418
+ const sectionsHtml = tabs.map(([tabId, , html]) => `<section id="${escapeHtml(tabId)}" class="tab">${html}</section>${REPORT_EOL}`).join("");
1419
+ const chartDataJson = escapeJsonForHtmlScript(JSON.stringify(buildDotnetChartData(nodeStats)));
1291
1420
  const testInfo = reportObject(nodeStats, "testInfo", "TestInfo");
1292
1421
  const template = `<!doctype html>
1293
1422
  <html lang="en">
@@ -1379,7 +1508,7 @@ const reportLogo=document.querySelector('[data-report-logo]');
1379
1508
  function applyReportTheme(theme){const normalized=theme==='dark'?'dark':'light';document.body.setAttribute('data-theme',normalized);if(reportLogo){const lightLogo=reportLogo.dataset.logoLight||reportLogo.getAttribute('src');const darkLogo=reportLogo.dataset.logoDark||reportLogo.getAttribute('src');reportLogo.setAttribute('src',normalized==='dark'?darkLogo:lightLogo);}if(reportThemeToggle){const darkActive=normalized==='dark';const nextLabel=darkActive?'light':'dark';reportThemeToggle.innerHTML=darkActive?'&#x2600;':'&#x263E;';reportThemeToggle.setAttribute('aria-pressed',darkActive?'true':'false');reportThemeToggle.setAttribute('aria-label','Switch to '+nextLabel+' theme');reportThemeToggle.setAttribute('title','Switch to '+nextLabel+' theme');}}
1380
1509
  function show(id){btns.forEach(b=>b.classList.toggle('active',b.dataset.tab===id));tabSections.forEach(t=>t.classList.toggle('active',t.id===id));}
1381
1510
  function formatMetric(v){if(!Number.isFinite(v))return '0';return Math.abs(v)>=100?v.toFixed(0):v.toFixed(2);}
1382
- function setupCanvas(canvas){const dpr=window.devicePixelRatio||1;const w=Math.max(320,canvas.clientWidth||320);const h=Math.max(220,canvas.clientHeight||220);canvas.width=Math.floor(w*dpr);canvas.height=Math.floor(h*dpr);const ctx=canvas.getContext('2d');ctx.setTransform(dpr,0,0,dpr,0,0);return {ctx,w,h};}
1511
+ function setupCanvas(canvas){const dpr=window.devicePixelRatio||1;const rect=canvas.getBoundingClientRect();const w=Math.max(1,Math.round(rect.width||canvas.clientWidth||320));const h=Math.max(1,Math.round(rect.height||canvas.clientHeight||220));canvas.width=Math.max(1,Math.round(w*dpr));canvas.height=Math.max(1,Math.round(h*dpr));const ctx=canvas.getContext('2d');ctx.setTransform(dpr,0,0,dpr,0,0);return {ctx,w,h};}
1383
1512
  function drawNoData(ctx,w,h,msg){ctx.fillStyle='#9fb0c3';ctx.font='13px Segoe UI';ctx.textAlign='center';ctx.fillText(msg,w/2,h/2);}
1384
1513
  function drawBar(canvasId,points){const canvas=document.getElementById(canvasId);if(!canvas)return;const c=setupCanvas(canvas);const ctx=c.ctx,w=c.w,h=c.h;ctx.clearRect(0,0,w,h);if(!points||points.length===0){drawNoData(ctx,w,h,'No data');return;}const left=46,right=14,top=16,bottom=62;const pw=w-left-right;const ph=h-top-bottom;const max=Math.max(...points.map(p=>p.value),1);ctx.strokeStyle='#334155';ctx.lineWidth=1;for(let i=0;i<=4;i++){const y=top+(ph*(i/4));ctx.beginPath();ctx.moveTo(left,y);ctx.lineTo(w-right,y);ctx.stroke();}const slot=pw/points.length;const bar=Math.max(8,slot*0.58);ctx.font='11px Segoe UI';for(let i=0;i<points.length;i++){const p=points[i];const x=left+i*slot+(slot-bar)/2;const bh=(p.value/max)*ph;const y=top+ph-bh;ctx.fillStyle=p.color||'#3b82f6';ctx.fillRect(x,y,bar,bh);ctx.fillStyle='#dbe6f4';ctx.textAlign='center';ctx.fillText(formatMetric(p.value),x+bar/2,Math.max(12,y-4));ctx.save();ctx.translate(x+bar/2,h-bottom+14);ctx.rotate(-0.6);ctx.fillStyle='#b5c2d3';ctx.fillText((p.label||'').slice(0,26),0,0);ctx.restore();}ctx.fillStyle='#b5c2d3';ctx.textAlign='right';for(let i=0;i<=4;i++){const value=max*(1-i/4);const y=top+(ph*(i/4))+4;ctx.fillText(formatMetric(value),left-6,y);}}
1385
1514
  function drawPie(canvasId,points){const canvas=document.getElementById(canvasId);if(!canvas)return;const c=setupCanvas(canvas);const ctx=c.ctx,w=c.w,h=c.h;ctx.clearRect(0,0,w,h);if(!points||points.length===0){drawNoData(ctx,w,h,'No data');return;}const total=points.reduce((s,p)=>s+(p.value||0),0);if(total<=0){drawNoData(ctx,w,h,'No data');return;}const cx=w*0.35,cy=h*0.5,r=Math.min(w,h)*0.28;let angle=-Math.PI/2;for(const p of points){const val=Math.max(0,p.value||0);const delta=(val/total)*Math.PI*2;ctx.beginPath();ctx.moveTo(cx,cy);ctx.arc(cx,cy,r,angle,angle+delta);ctx.closePath();ctx.fillStyle=p.color||'#3b82f6';ctx.fill();angle+=delta;}ctx.fillStyle='#0f172a';ctx.beginPath();ctx.arc(cx,cy,r*0.54,0,Math.PI*2);ctx.fill();ctx.fillStyle='#e5eefc';ctx.font='bold 18px Segoe UI';ctx.textAlign='center';ctx.fillText(total.toString(),cx,cy+6);ctx.font='12px Segoe UI';ctx.fillStyle='#9fb0c3';ctx.fillText('requests',cx,cy+24);ctx.textAlign='left';let y=cy-r+10;for(const p of points){ctx.fillStyle=p.color||'#3b82f6';ctx.fillRect(w*0.64,y-10,12,12);ctx.fillStyle='#e6edf3';ctx.font='12px Segoe UI';const pct=total<=0?0:((p.value/total)*100);ctx.fillText(\`\${p.label}: \${p.value} (\${pct.toFixed(1)}%)\`,w*0.64+18,y);y+=20;}}