@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.
- package/README.md +6 -0
- package/dist/cjs/iteration-observation-diagnostics.js +513 -0
- package/dist/cjs/iteration-observations.js +195 -22
- package/dist/cjs/local-report-input.js +21 -0
- package/dist/cjs/local.js +55 -10
- package/dist/cjs/report-history.js +421 -0
- package/dist/cjs/reporting-svg.js +116 -0
- package/dist/cjs/reporting.js +436 -140
- package/dist/cjs/runtime.js +313 -85
- package/dist/cjs/sink-retry-policy.js +52 -0
- package/dist/cjs/sinks.js +112 -9
- package/dist/cjs/transports.js +78 -25
- package/dist/esm/iteration-observation-diagnostics.js +508 -0
- package/dist/esm/iteration-observations.js +195 -22
- package/dist/esm/local-report-input.js +17 -0
- package/dist/esm/local.js +55 -10
- package/dist/esm/report-history.js +413 -0
- package/dist/esm/reporting-svg.js +113 -0
- package/dist/esm/reporting.js +436 -140
- package/dist/esm/runtime.js +313 -85
- package/dist/esm/sink-retry-policy.js +44 -0
- package/dist/esm/sinks.js +112 -9
- package/dist/esm/transports.js +78 -25
- package/dist/types/iteration-observation-diagnostics.d.ts +21 -0
- package/dist/types/iteration-observations.d.ts +5 -0
- package/dist/types/local-report-input.d.ts +6 -0
- package/dist/types/report-history.d.ts +124 -0
- package/dist/types/reporting-svg.d.ts +2 -0
- package/dist/types/reporting.d.ts +6 -3
- package/dist/types/runtime.d.ts +25 -0
- package/dist/types/sink-retry-policy.d.ts +9 -0
- package/dist/types/sinks.d.ts +6 -0
- package/dist/types/transports.d.ts +4 -0
- package/package.json +2 -2
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
3574
|
+
throw new ReportingSinkHttpError(sinkName, response.status, readHttpResponseStatusText(response), readHttpResponseRequestId(response));
|
|
3539
3575
|
}
|
|
3540
|
-
return
|
|
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
|
|
3548
|
-
if (typeof
|
|
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
|
|
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,
|
package/dist/esm/transports.js
CHANGED
|
@@ -596,14 +596,10 @@ class WebSocketEndpointDefinitionModel extends TrafficEndpointDefinitionModel {
|
|
|
596
596
|
parsed = new URL(url);
|
|
597
597
|
}
|
|
598
598
|
catch {
|
|
599
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3580
|
+
current = current.at(index);
|
|
3583
3581
|
continue;
|
|
3584
3582
|
}
|
|
3585
|
-
if (!isRecord(current)
|
|
3583
|
+
if (!isRecord(current)) {
|
|
3586
3584
|
return null;
|
|
3587
3585
|
}
|
|
3588
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
3995
|
-
const segments = path
|
|
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
|
-
|
|
4015
|
+
let next = readOwnJsonProperty(current, segment);
|
|
4003
4016
|
if (!isRecord(next)) {
|
|
4004
|
-
|
|
4017
|
+
next = {};
|
|
4018
|
+
defineJsonProperty(current, segment, next);
|
|
4005
4019
|
}
|
|
4006
|
-
current =
|
|
4020
|
+
current = next;
|
|
4007
4021
|
}
|
|
4008
|
-
current
|
|
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 {};
|
|
@@ -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
|
-
*
|
|
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;
|