@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.
@@ -0,0 +1,44 @@
1
+ export const DEFAULT_SINK_RETRY_COUNT = 3;
2
+ export const DEFAULT_SINK_RETRY_BACKOFF_MS = 250;
3
+ export const MAXIMUM_SINK_RETRY_COUNT = 100;
4
+ export const MAXIMUM_TIMER_DELAY_MS = 2147483647;
5
+ export function normalizeSinkRetryCount(value) {
6
+ const resolved = value === undefined ? DEFAULT_SINK_RETRY_COUNT : value;
7
+ if (!Number.isFinite(resolved) || resolved <= 0) {
8
+ return 0;
9
+ }
10
+ return Math.min(Math.trunc(resolved), MAXIMUM_SINK_RETRY_COUNT);
11
+ }
12
+ export function normalizeSinkRetryBackoffMs(value) {
13
+ const resolved = value === undefined ? DEFAULT_SINK_RETRY_BACKOFF_MS : value;
14
+ if (!Number.isFinite(resolved) || resolved <= 0) {
15
+ return 0;
16
+ }
17
+ return Math.min(Math.trunc(resolved), MAXIMUM_TIMER_DELAY_MS);
18
+ }
19
+ export function sinkRetryDelayMs(baseDelayMs, retryNumber) {
20
+ if (baseDelayMs <= 0) {
21
+ return 0;
22
+ }
23
+ let delayMs = Math.min(Math.trunc(baseDelayMs), MAXIMUM_TIMER_DELAY_MS);
24
+ const doublings = Math.max(Math.trunc(retryNumber) - 1, 0);
25
+ for (let index = 0; index < doublings; index += 1) {
26
+ if (delayMs >= Math.ceil(MAXIMUM_TIMER_DELAY_MS / 2)) {
27
+ return MAXIMUM_TIMER_DELAY_MS;
28
+ }
29
+ delayMs *= 2;
30
+ }
31
+ return delayMs;
32
+ }
33
+ export async function waitForSinkRetryDelay(delayMs) {
34
+ if (delayMs <= 0) {
35
+ await cooperativeSinkRetryYield();
36
+ return;
37
+ }
38
+ await new Promise((resolve) => {
39
+ setTimeout(resolve, Math.min(delayMs, MAXIMUM_TIMER_DELAY_MS));
40
+ });
41
+ }
42
+ export async function cooperativeSinkRetryYield() {
43
+ await new Promise((resolve) => setImmediate(resolve));
44
+ }
package/dist/esm/sinks.js CHANGED
@@ -6,7 +6,26 @@ import { createSocket } from "node:dgram";
6
6
  import { gzip } from "node:zlib";
7
7
  import { Pool } from "pg";
8
8
  import { serializeIterationObservationBatchGzipJson } from "./iteration-observations.js";
9
+ import { redactIterationObservationSecrets } from "./iteration-observation-diagnostics.js";
9
10
  const PORTAL_RUN_TOKEN_PROVIDER = Symbol.for("loadstrike.internal.portal-run-token-provider");
11
+ const MAXIMUM_HTTP_RESPONSE_BODY_BYTES = 256 * 1024;
12
+ const MAXIMUM_HTTP_METADATA_CHARACTERS = 256;
13
+ const HTTP_METADATA_TRUNCATION_SUFFIX = " [truncated]";
14
+ class ReportingSinkHttpError extends Error {
15
+ constructor(sinkName, status, statusText, requestId) {
16
+ const normalizedStatus = Number.isFinite(status) ? Math.max(Math.trunc(status), 0) : 0;
17
+ const normalizedStatusText = normalizeHttpMetadata(statusText);
18
+ const normalizedRequestId = normalizeHttpMetadata(requestId);
19
+ super(`${sinkName} write failed with HTTP status ${normalizedStatus}`
20
+ + (normalizedStatusText ? ` ${normalizedStatusText}` : "")
21
+ + (normalizedRequestId ? ` (requestId=${normalizedRequestId})` : "")
22
+ + ".");
23
+ this.name = "ReportingSinkHttpError";
24
+ this.status = normalizedStatus;
25
+ this.statusText = normalizedStatusText;
26
+ this.requestId = normalizedRequestId;
27
+ }
28
+ }
10
29
  function gzipJsonAsync(value) {
11
30
  const json = Buffer.from(JSON.stringify(value), "utf8");
12
31
  return new Promise((resolve, reject) => {
@@ -2202,6 +2221,7 @@ function removeSdkPercentileFields(event) {
2202
2221
  }
2203
2222
  function createIterationObservationEvents(session, batch) {
2204
2223
  const events = [];
2224
+ const occurredUtc = dateFromUtcNanoseconds(batch.createdUtcNs);
2205
2225
  for (const observation of batch.observations) {
2206
2226
  const tags = {
2207
2227
  run_id: batch.runId,
@@ -2214,7 +2234,7 @@ function createIterationObservationEvents(session, batch) {
2214
2234
  events.push({
2215
2235
  runId: batch.runId,
2216
2236
  eventType: "iteration.observation",
2217
- occurredUtc: new Date(),
2237
+ occurredUtc,
2218
2238
  sessionId: batch.sessionId,
2219
2239
  testSuite: session.testSuite,
2220
2240
  testName: session.testName,
@@ -2249,7 +2269,7 @@ function createIterationObservationEvents(session, batch) {
2249
2269
  events.push({
2250
2270
  runId: batch.runId,
2251
2271
  eventType: "iteration.step",
2252
- occurredUtc: new Date(),
2272
+ occurredUtc,
2253
2273
  sessionId: batch.sessionId,
2254
2274
  testSuite: session.testSuite,
2255
2275
  testName: session.testName,
@@ -2280,10 +2300,11 @@ function createIterationObservationEvents(session, batch) {
2280
2300
  return events;
2281
2301
  }
2282
2302
  function createIterationCompletionEvents(session, completion) {
2303
+ const occurredUtc = dateFromUtcNanoseconds(completion.completedUtcNs);
2283
2304
  return [{
2284
2305
  runId: completion.runId,
2285
2306
  eventType: "observation.stream.completed",
2286
- occurredUtc: new Date(),
2307
+ occurredUtc,
2287
2308
  sessionId: completion.sessionId,
2288
2309
  testSuite: session.testSuite,
2289
2310
  testName: session.testName,
@@ -2342,6 +2363,22 @@ function portalEventId(event, index) {
2342
2363
  });
2343
2364
  return `lsr_${createHash("sha256").update(material).digest("hex").slice(0, 40)}`;
2344
2365
  }
2366
+ function dateFromUtcNanoseconds(value) {
2367
+ try {
2368
+ const nanoseconds = BigInt(value);
2369
+ if (nanoseconds < 0n) {
2370
+ return new Date(0);
2371
+ }
2372
+ const milliseconds = nanoseconds / 1000000n;
2373
+ const numeric = Number(milliseconds);
2374
+ return Number.isFinite(numeric) && numeric <= 8640000000000000
2375
+ ? new Date(numeric)
2376
+ : new Date(0);
2377
+ }
2378
+ catch {
2379
+ return new Date(0);
2380
+ }
2381
+ }
2345
2382
  function addMeasurementFields(fields, prefix, measurement) {
2346
2383
  const request = measurement?.request ?? { count: 0, percent: 0, rps: 0 };
2347
2384
  const latency = measurement?.latency ?? {
@@ -3533,22 +3570,85 @@ async function postWithTimeout(fetchImpl, url, init, timeoutMs, sinkName) {
3533
3570
  const timer = setTimeout(() => controller.abort(), Math.max(timeoutMs, 1));
3534
3571
  try {
3535
3572
  const response = await fetchImpl(url, { ...init, signal: controller.signal });
3536
- const body = await readResponseBodyText(response);
3537
3573
  if (!response.ok) {
3538
- throw new Error(`${sinkName} write failed with status ${response.status}: ${body}`);
3574
+ throw new ReportingSinkHttpError(sinkName, response.status, readHttpResponseStatusText(response), readHttpResponseRequestId(response));
3539
3575
  }
3540
- return body;
3576
+ return await readResponseBodyText(response);
3541
3577
  }
3542
3578
  finally {
3543
3579
  clearTimeout(timer);
3544
3580
  }
3545
3581
  }
3546
3582
  async function readResponseBodyText(response) {
3547
- const candidate = response;
3548
- if (typeof candidate.text !== "function") {
3583
+ const body = response.body;
3584
+ if (body && typeof body.getReader === "function") {
3585
+ const reader = body.getReader();
3586
+ const chunks = [];
3587
+ let bytes = 0;
3588
+ try {
3589
+ while (true) {
3590
+ const next = await reader.read();
3591
+ if (next.done)
3592
+ break;
3593
+ if (!next.value)
3594
+ continue;
3595
+ if (bytes + next.value.byteLength > MAXIMUM_HTTP_RESPONSE_BODY_BYTES) {
3596
+ await reader.cancel();
3597
+ throw new Error("Reporting sink response exceeded the bounded response limit.");
3598
+ }
3599
+ chunks.push(next.value);
3600
+ bytes += next.value.byteLength;
3601
+ }
3602
+ }
3603
+ finally {
3604
+ reader.releaseLock();
3605
+ }
3606
+ return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
3607
+ }
3608
+ return "";
3609
+ }
3610
+ function readHttpResponseStatusText(response) {
3611
+ try {
3612
+ return normalizeHttpMetadata(response.statusText);
3613
+ }
3614
+ catch {
3615
+ return "";
3616
+ }
3617
+ }
3618
+ function readHttpResponseRequestId(response) {
3619
+ for (const name of [
3620
+ "x-request-id",
3621
+ "x-correlation-id",
3622
+ "request-id",
3623
+ "traceparent"
3624
+ ]) {
3625
+ try {
3626
+ const value = response.headers?.get(name);
3627
+ if (value) {
3628
+ return normalizeHttpMetadata(value);
3629
+ }
3630
+ }
3631
+ catch {
3632
+ return "";
3633
+ }
3634
+ }
3635
+ return "";
3636
+ }
3637
+ function normalizeHttpMetadata(value) {
3638
+ let text;
3639
+ try {
3640
+ const source = String(value ?? "");
3641
+ text = source.length <= MAXIMUM_HTTP_METADATA_CHARACTERS
3642
+ ? source
3643
+ : source.slice(0, MAXIMUM_HTTP_METADATA_CHARACTERS - HTTP_METADATA_TRUNCATION_SUFFIX.length) + HTTP_METADATA_TRUNCATION_SUFFIX;
3644
+ }
3645
+ catch {
3549
3646
  return "";
3550
3647
  }
3551
- return String(await candidate.text());
3648
+ return redactIterationObservationSecrets(Buffer.from(text, "utf8").toString("utf8")
3649
+ .replace(/[\u0000-\u001F\u007F-\u009F]/gu, " ")
3650
+ .replace(/\s{2,}/gu, " ")
3651
+ .trim());
3552
3652
  }
3553
3653
  function validatePortalIngestResponse(responseBody) {
3554
3654
  const text = String(responseBody ?? "").trim();
@@ -3601,6 +3701,8 @@ export const __loadstrikeTestExports = {
3601
3701
  cloneScenarioStats,
3602
3702
  cloneSessionStartInfo,
3603
3703
  createFinalStatsEvents,
3704
+ createIterationCompletionEvents,
3705
+ createIterationObservationEvents,
3604
3706
  createRealtimeStatsEvents,
3605
3707
  createRunResultEvents,
3606
3708
  createReportingEvent,
@@ -3617,6 +3719,7 @@ export const __loadstrikeTestExports = {
3617
3719
  normalizeStringMap,
3618
3720
  optionNumber,
3619
3721
  optionString,
3722
+ portalEventPayload,
3620
3723
  pickBooleanValue,
3621
3724
  pickRecordValue,
3622
3725
  postWithTimeout,
@@ -596,14 +596,10 @@ class WebSocketEndpointDefinitionModel extends TrafficEndpointDefinitionModel {
596
596
  parsed = new URL(url);
597
597
  }
598
598
  catch {
599
- // Diagnostic text only; no connection is opened here.
600
- // nosemgrep: vulnerability-tools.semgrep-rules.javascript.lang.security.detect-insecure-websocket
601
- throw new Error("Url must be an absolute ws:// or wss:// URI.");
599
+ throw new Error("Url must be an absolute WebSocket URI using the ws or wss scheme.");
602
600
  }
603
601
  if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
604
- // Diagnostic text only; no connection is opened here.
605
- // nosemgrep: vulnerability-tools.semgrep-rules.javascript.lang.security.detect-insecure-websocket
606
- throw new Error("Url must be an absolute ws:// or wss:// URI.");
602
+ throw new Error("Url must be an absolute WebSocket URI using the ws or wss scheme.");
607
603
  }
608
604
  if (this.ConnectTimeoutSeconds <= 0) {
609
605
  throw new RangeError("ConnectTimeout must be greater than zero.");
@@ -3132,14 +3128,10 @@ function validateWebSocketEndpoint(endpoint, mode) {
3132
3128
  parsed = new URL(url);
3133
3129
  }
3134
3130
  catch {
3135
- // Diagnostic text only; no connection is opened here.
3136
- // nosemgrep: vulnerability-tools.semgrep-rules.javascript.lang.security.detect-insecure-websocket
3137
- throw new Error("Url must be an absolute ws:// or wss:// URI.");
3131
+ throw new Error("Url must be an absolute WebSocket URI using the ws or wss scheme.");
3138
3132
  }
3139
3133
  if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
3140
- // Diagnostic text only; no connection is opened here.
3141
- // nosemgrep: vulnerability-tools.semgrep-rules.javascript.lang.security.detect-insecure-websocket
3142
- throw new Error("Url must be an absolute ws:// or wss:// URI.");
3134
+ throw new Error("Url must be an absolute WebSocket URI using the ws or wss scheme.");
3143
3135
  }
3144
3136
  const connectMs = optionNumber(options, "ConnectTimeoutMs", "connectTimeoutMs");
3145
3137
  const connectSeconds = optionNumber(options, "ConnectTimeoutSeconds", "connectTimeoutSeconds", "ConnectTimeout", "connectTimeout");
@@ -3569,7 +3561,13 @@ function extractJsonPath(value, arrayPath) {
3569
3561
  if (!normalized) {
3570
3562
  return value;
3571
3563
  }
3572
- const tokens = normalized.replace(/\[(\d+)\]/g, ".$1").split(".").filter((x) => x.length > 0);
3564
+ let tokens;
3565
+ try {
3566
+ tokens = safeJsonPathSegments(normalized.replace(/\[(\d+)\]/g, ".$1"));
3567
+ }
3568
+ catch {
3569
+ return null;
3570
+ }
3573
3571
  for (const token of tokens) {
3574
3572
  if (current == null) {
3575
3573
  return null;
@@ -3579,13 +3577,17 @@ function extractJsonPath(value, arrayPath) {
3579
3577
  if (!Number.isInteger(index) || index < 0 || index >= current.length) {
3580
3578
  return null;
3581
3579
  }
3582
- current = current[index];
3580
+ current = current.at(index);
3583
3581
  continue;
3584
3582
  }
3585
- if (!isRecord(current) || !(token in current)) {
3583
+ if (!isRecord(current)) {
3586
3584
  return null;
3587
3585
  }
3588
- current = current[token];
3586
+ const descriptor = Object.getOwnPropertyDescriptor(current, token);
3587
+ if (!descriptor || !("value" in descriptor)) {
3588
+ return null;
3589
+ }
3590
+ current = descriptor.value;
3589
3591
  }
3590
3592
  return current;
3591
3593
  }
@@ -3602,7 +3604,7 @@ function parseMaybeJson(value) {
3602
3604
  }
3603
3605
  }
3604
3606
  function headersToRecord(value) {
3605
- const headers = {};
3607
+ const headers = Object.create(null);
3606
3608
  value.forEach((headerValue, key) => {
3607
3609
  headers[key] = headerValue;
3608
3610
  });
@@ -3956,11 +3958,22 @@ function extractTrackingValue(payload, selector) {
3956
3958
  return null;
3957
3959
  }
3958
3960
  let current = body;
3959
- for (const segment of selector.slice("json:".length).trim().replace(/^\$\./, "").split(".").filter(Boolean)) {
3961
+ const path = selector.slice("json:".length).trim().replace(/^\$\./, "");
3962
+ let segments;
3963
+ try {
3964
+ segments = safeJsonPathSegments(path);
3965
+ }
3966
+ catch {
3967
+ return null;
3968
+ }
3969
+ for (const segment of segments) {
3960
3970
  if (!isRecord(current)) {
3961
3971
  return null;
3962
3972
  }
3963
- current = current[segment];
3973
+ if (!Object.prototype.hasOwnProperty.call(current, segment)) {
3974
+ return null;
3975
+ }
3976
+ current = readOwnJsonProperty(current, segment);
3964
3977
  }
3965
3978
  return current == null ? null : String(current);
3966
3979
  }
@@ -3991,23 +4004,61 @@ function injectTrackingValue(payload, selector, value) {
3991
4004
  }
3992
4005
  function setJsonBodyValue(body, path, value) {
3993
4006
  const target = parseBodyObject(body) ?? {};
3994
- const clone = cloneBody(target);
3995
- const segments = path.split(".").filter(Boolean);
4007
+ const clone = cloneJsonRecord(target);
4008
+ const segments = safeJsonPathSegments(path);
3996
4009
  if (!segments.length) {
3997
4010
  return clone;
3998
4011
  }
3999
4012
  let current = clone;
4000
4013
  for (let i = 0; i < segments.length - 1; i += 1) {
4001
4014
  const segment = segments[i];
4002
- const next = current[segment];
4015
+ let next = readOwnJsonProperty(current, segment);
4003
4016
  if (!isRecord(next)) {
4004
- current[segment] = {};
4017
+ next = {};
4018
+ defineJsonProperty(current, segment, next);
4005
4019
  }
4006
- current = current[segment];
4020
+ current = next;
4007
4021
  }
4008
- current[segments[segments.length - 1]] = value;
4022
+ defineJsonProperty(current, segments[segments.length - 1], value);
4009
4023
  return clone;
4010
4024
  }
4025
+ const FORBIDDEN_JSON_PATH_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
4026
+ function safeJsonPathSegments(path) {
4027
+ const segments = path.split(".").filter(Boolean);
4028
+ const forbidden = segments.find((segment) => FORBIDDEN_JSON_PATH_SEGMENTS.has(segment));
4029
+ if (forbidden) {
4030
+ throw new Error(`Tracking selector contains forbidden JSON path segment '${forbidden}'.`);
4031
+ }
4032
+ return segments;
4033
+ }
4034
+ function defineJsonProperty(target, key, value) {
4035
+ Object.defineProperty(target, key, {
4036
+ configurable: true,
4037
+ enumerable: true,
4038
+ value,
4039
+ writable: true
4040
+ });
4041
+ }
4042
+ function readOwnJsonProperty(target, key) {
4043
+ const descriptor = Object.getOwnPropertyDescriptor(target, key);
4044
+ return descriptor && "value" in descriptor ? descriptor.value : undefined;
4045
+ }
4046
+ function cloneJsonRecord(source) {
4047
+ const target = {};
4048
+ for (const [key, value] of Object.entries(source)) {
4049
+ defineJsonProperty(target, key, cloneJsonValue(value));
4050
+ }
4051
+ return target;
4052
+ }
4053
+ function cloneJsonValue(value) {
4054
+ if (Array.isArray(value)) {
4055
+ return value.map((entry) => cloneJsonValue(entry));
4056
+ }
4057
+ if (isRecord(value)) {
4058
+ return cloneJsonRecord(value);
4059
+ }
4060
+ return value;
4061
+ }
4011
4062
  function parseBodyObject(body) {
4012
4063
  if (body instanceof Uint8Array) {
4013
4064
  return parseBodyObject(new TextDecoder().decode(body));
@@ -4616,8 +4667,10 @@ export const __loadstrikeTestExports = {
4616
4667
  createNatsHeaders,
4617
4668
  createRedisStreamPayload,
4618
4669
  deserializeBrokerPayloadBody,
4670
+ extractJsonPath,
4619
4671
  extractTrackingValue,
4620
4672
  fromKafkaHeaders,
4673
+ headersToRecord,
4621
4674
  headerValue,
4622
4675
  attachDotNetTrackingPayloadAliases,
4623
4676
  attachPayloadHelpers,
@@ -0,0 +1,21 @@
1
+ export interface IterationObservationDiagnosticLogger {
2
+ debug(message: string): void | PromiseLike<void>;
3
+ warn(message: string): void | PromiseLike<void>;
4
+ error(message: string): void | PromiseLike<void>;
5
+ }
6
+ export interface IterationObservationDiagnosticContext {
7
+ sinkName: string;
8
+ operation: "observation-batch" | "observation-stream-completion" | "reporting-sink-action";
9
+ phase?: string;
10
+ runId: string;
11
+ resultOwnerId: string;
12
+ batchId?: string;
13
+ batchSequence64?: string;
14
+ observationCount: number;
15
+ attempt: number;
16
+ maximumAttempts: number;
17
+ nextDelayMs?: number;
18
+ }
19
+ export declare function logIterationObservationFailure(logger: IterationObservationDiagnosticLogger | undefined, level: "warn" | "error", context: IterationObservationDiagnosticContext, error: unknown): void;
20
+ export declare function logIterationObservationRecovery(logger: IterationObservationDiagnosticLogger | undefined, context: IterationObservationDiagnosticContext): void;
21
+ export declare function redactIterationObservationSecrets(value: string): string;
@@ -1,3 +1,4 @@
1
+ import { type IterationObservationDiagnosticLogger } from "./iteration-observation-diagnostics.js";
1
2
  export declare const ITERATION_OBSERVATION_SCHEMA_VERSION = "loadstrike.iteration-observation/1";
2
3
  export declare const ITERATION_OBSERVATION_BATCH_SCHEMA_VERSION = "loadstrike.iteration-batch/1";
3
4
  export declare const ITERATION_OBSERVATION_STREAM_COMPLETION_SCHEMA_VERSION = "loadstrike.iteration-stream-completion/1";
@@ -192,6 +193,9 @@ export interface IterationObservationReporterOptions {
192
193
  buffer?: ProcessIterationObservationBuffer;
193
194
  clock?: IterationObservationClock;
194
195
  skipValidation?: boolean;
196
+ sinkRetryCount?: number;
197
+ sinkRetryBackoffMs?: number;
198
+ logger?: IterationObservationDiagnosticLogger;
195
199
  }
196
200
  export declare class IterationObservationReporter {
197
201
  private readonly options;
@@ -215,6 +219,7 @@ export declare class IterationObservationReporter {
215
219
  capture(observation: LoadStrikeIterationObservationV1): boolean;
216
220
  flushNow(): void;
217
221
  sealAndDrain(): Promise<IterationObservationDeliveryResult>;
222
+ private completeWithRetries;
218
223
  buildWarnings(): IterationObservationGeneratorWarning[];
219
224
  private createBatch;
220
225
  private createCompletion;
@@ -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;