@loadstrike/loadstrike-sdk 1.0.30401 → 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/README.md +4 -0
- package/dist/cjs/iteration-observation-diagnostics.js +513 -0
- package/dist/cjs/iteration-observations.js +195 -22
- package/dist/cjs/local.js +55 -10
- package/dist/cjs/reporting.js +26 -7
- package/dist/cjs/runtime.js +159 -80
- 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.js +55 -10
- package/dist/esm/reporting.js +26 -7
- package/dist/esm/runtime.js +159 -80
- 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/runtime.d.ts +2 -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
package/dist/cjs/sinks.js
CHANGED
|
@@ -10,7 +10,26 @@ const node_dgram_1 = require("node:dgram");
|
|
|
10
10
|
const node_zlib_1 = require("node:zlib");
|
|
11
11
|
const pg_1 = require("pg");
|
|
12
12
|
const iteration_observations_js_1 = require("./iteration-observations.js");
|
|
13
|
+
const iteration_observation_diagnostics_js_1 = require("./iteration-observation-diagnostics.js");
|
|
13
14
|
const PORTAL_RUN_TOKEN_PROVIDER = Symbol.for("loadstrike.internal.portal-run-token-provider");
|
|
15
|
+
const MAXIMUM_HTTP_RESPONSE_BODY_BYTES = 256 * 1024;
|
|
16
|
+
const MAXIMUM_HTTP_METADATA_CHARACTERS = 256;
|
|
17
|
+
const HTTP_METADATA_TRUNCATION_SUFFIX = " [truncated]";
|
|
18
|
+
class ReportingSinkHttpError extends Error {
|
|
19
|
+
constructor(sinkName, status, statusText, requestId) {
|
|
20
|
+
const normalizedStatus = Number.isFinite(status) ? Math.max(Math.trunc(status), 0) : 0;
|
|
21
|
+
const normalizedStatusText = normalizeHttpMetadata(statusText);
|
|
22
|
+
const normalizedRequestId = normalizeHttpMetadata(requestId);
|
|
23
|
+
super(`${sinkName} write failed with HTTP status ${normalizedStatus}`
|
|
24
|
+
+ (normalizedStatusText ? ` ${normalizedStatusText}` : "")
|
|
25
|
+
+ (normalizedRequestId ? ` (requestId=${normalizedRequestId})` : "")
|
|
26
|
+
+ ".");
|
|
27
|
+
this.name = "ReportingSinkHttpError";
|
|
28
|
+
this.status = normalizedStatus;
|
|
29
|
+
this.statusText = normalizedStatusText;
|
|
30
|
+
this.requestId = normalizedRequestId;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
14
33
|
function gzipJsonAsync(value) {
|
|
15
34
|
const json = Buffer.from(JSON.stringify(value), "utf8");
|
|
16
35
|
return new Promise((resolve, reject) => {
|
|
@@ -2234,6 +2253,7 @@ function removeSdkPercentileFields(event) {
|
|
|
2234
2253
|
}
|
|
2235
2254
|
function createIterationObservationEvents(session, batch) {
|
|
2236
2255
|
const events = [];
|
|
2256
|
+
const occurredUtc = dateFromUtcNanoseconds(batch.createdUtcNs);
|
|
2237
2257
|
for (const observation of batch.observations) {
|
|
2238
2258
|
const tags = {
|
|
2239
2259
|
run_id: batch.runId,
|
|
@@ -2246,7 +2266,7 @@ function createIterationObservationEvents(session, batch) {
|
|
|
2246
2266
|
events.push({
|
|
2247
2267
|
runId: batch.runId,
|
|
2248
2268
|
eventType: "iteration.observation",
|
|
2249
|
-
occurredUtc
|
|
2269
|
+
occurredUtc,
|
|
2250
2270
|
sessionId: batch.sessionId,
|
|
2251
2271
|
testSuite: session.testSuite,
|
|
2252
2272
|
testName: session.testName,
|
|
@@ -2281,7 +2301,7 @@ function createIterationObservationEvents(session, batch) {
|
|
|
2281
2301
|
events.push({
|
|
2282
2302
|
runId: batch.runId,
|
|
2283
2303
|
eventType: "iteration.step",
|
|
2284
|
-
occurredUtc
|
|
2304
|
+
occurredUtc,
|
|
2285
2305
|
sessionId: batch.sessionId,
|
|
2286
2306
|
testSuite: session.testSuite,
|
|
2287
2307
|
testName: session.testName,
|
|
@@ -2312,10 +2332,11 @@ function createIterationObservationEvents(session, batch) {
|
|
|
2312
2332
|
return events;
|
|
2313
2333
|
}
|
|
2314
2334
|
function createIterationCompletionEvents(session, completion) {
|
|
2335
|
+
const occurredUtc = dateFromUtcNanoseconds(completion.completedUtcNs);
|
|
2315
2336
|
return [{
|
|
2316
2337
|
runId: completion.runId,
|
|
2317
2338
|
eventType: "observation.stream.completed",
|
|
2318
|
-
occurredUtc
|
|
2339
|
+
occurredUtc,
|
|
2319
2340
|
sessionId: completion.sessionId,
|
|
2320
2341
|
testSuite: session.testSuite,
|
|
2321
2342
|
testName: session.testName,
|
|
@@ -2374,6 +2395,22 @@ function portalEventId(event, index) {
|
|
|
2374
2395
|
});
|
|
2375
2396
|
return `lsr_${(0, node_crypto_1.createHash)("sha256").update(material).digest("hex").slice(0, 40)}`;
|
|
2376
2397
|
}
|
|
2398
|
+
function dateFromUtcNanoseconds(value) {
|
|
2399
|
+
try {
|
|
2400
|
+
const nanoseconds = BigInt(value);
|
|
2401
|
+
if (nanoseconds < 0n) {
|
|
2402
|
+
return new Date(0);
|
|
2403
|
+
}
|
|
2404
|
+
const milliseconds = nanoseconds / 1000000n;
|
|
2405
|
+
const numeric = Number(milliseconds);
|
|
2406
|
+
return Number.isFinite(numeric) && numeric <= 8640000000000000
|
|
2407
|
+
? new Date(numeric)
|
|
2408
|
+
: new Date(0);
|
|
2409
|
+
}
|
|
2410
|
+
catch {
|
|
2411
|
+
return new Date(0);
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2377
2414
|
function addMeasurementFields(fields, prefix, measurement) {
|
|
2378
2415
|
const request = measurement?.request ?? { count: 0, percent: 0, rps: 0 };
|
|
2379
2416
|
const latency = measurement?.latency ?? {
|
|
@@ -3565,22 +3602,85 @@ async function postWithTimeout(fetchImpl, url, init, timeoutMs, sinkName) {
|
|
|
3565
3602
|
const timer = setTimeout(() => controller.abort(), Math.max(timeoutMs, 1));
|
|
3566
3603
|
try {
|
|
3567
3604
|
const response = await fetchImpl(url, { ...init, signal: controller.signal });
|
|
3568
|
-
const body = await readResponseBodyText(response);
|
|
3569
3605
|
if (!response.ok) {
|
|
3570
|
-
throw new
|
|
3606
|
+
throw new ReportingSinkHttpError(sinkName, response.status, readHttpResponseStatusText(response), readHttpResponseRequestId(response));
|
|
3571
3607
|
}
|
|
3572
|
-
return
|
|
3608
|
+
return await readResponseBodyText(response);
|
|
3573
3609
|
}
|
|
3574
3610
|
finally {
|
|
3575
3611
|
clearTimeout(timer);
|
|
3576
3612
|
}
|
|
3577
3613
|
}
|
|
3578
3614
|
async function readResponseBodyText(response) {
|
|
3579
|
-
const
|
|
3580
|
-
if (typeof
|
|
3615
|
+
const body = response.body;
|
|
3616
|
+
if (body && typeof body.getReader === "function") {
|
|
3617
|
+
const reader = body.getReader();
|
|
3618
|
+
const chunks = [];
|
|
3619
|
+
let bytes = 0;
|
|
3620
|
+
try {
|
|
3621
|
+
while (true) {
|
|
3622
|
+
const next = await reader.read();
|
|
3623
|
+
if (next.done)
|
|
3624
|
+
break;
|
|
3625
|
+
if (!next.value)
|
|
3626
|
+
continue;
|
|
3627
|
+
if (bytes + next.value.byteLength > MAXIMUM_HTTP_RESPONSE_BODY_BYTES) {
|
|
3628
|
+
await reader.cancel();
|
|
3629
|
+
throw new Error("Reporting sink response exceeded the bounded response limit.");
|
|
3630
|
+
}
|
|
3631
|
+
chunks.push(next.value);
|
|
3632
|
+
bytes += next.value.byteLength;
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
finally {
|
|
3636
|
+
reader.releaseLock();
|
|
3637
|
+
}
|
|
3638
|
+
return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
|
|
3639
|
+
}
|
|
3640
|
+
return "";
|
|
3641
|
+
}
|
|
3642
|
+
function readHttpResponseStatusText(response) {
|
|
3643
|
+
try {
|
|
3644
|
+
return normalizeHttpMetadata(response.statusText);
|
|
3645
|
+
}
|
|
3646
|
+
catch {
|
|
3647
|
+
return "";
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
function readHttpResponseRequestId(response) {
|
|
3651
|
+
for (const name of [
|
|
3652
|
+
"x-request-id",
|
|
3653
|
+
"x-correlation-id",
|
|
3654
|
+
"request-id",
|
|
3655
|
+
"traceparent"
|
|
3656
|
+
]) {
|
|
3657
|
+
try {
|
|
3658
|
+
const value = response.headers?.get(name);
|
|
3659
|
+
if (value) {
|
|
3660
|
+
return normalizeHttpMetadata(value);
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
catch {
|
|
3664
|
+
return "";
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
return "";
|
|
3668
|
+
}
|
|
3669
|
+
function normalizeHttpMetadata(value) {
|
|
3670
|
+
let text;
|
|
3671
|
+
try {
|
|
3672
|
+
const source = String(value ?? "");
|
|
3673
|
+
text = source.length <= MAXIMUM_HTTP_METADATA_CHARACTERS
|
|
3674
|
+
? source
|
|
3675
|
+
: source.slice(0, MAXIMUM_HTTP_METADATA_CHARACTERS - HTTP_METADATA_TRUNCATION_SUFFIX.length) + HTTP_METADATA_TRUNCATION_SUFFIX;
|
|
3676
|
+
}
|
|
3677
|
+
catch {
|
|
3581
3678
|
return "";
|
|
3582
3679
|
}
|
|
3583
|
-
return
|
|
3680
|
+
return (0, iteration_observation_diagnostics_js_1.redactIterationObservationSecrets)(Buffer.from(text, "utf8").toString("utf8")
|
|
3681
|
+
.replace(/[\u0000-\u001F\u007F-\u009F]/gu, " ")
|
|
3682
|
+
.replace(/\s{2,}/gu, " ")
|
|
3683
|
+
.trim());
|
|
3584
3684
|
}
|
|
3585
3685
|
function validatePortalIngestResponse(responseBody) {
|
|
3586
3686
|
const text = String(responseBody ?? "").trim();
|
|
@@ -3633,6 +3733,8 @@ exports.__loadstrikeTestExports = {
|
|
|
3633
3733
|
cloneScenarioStats,
|
|
3634
3734
|
cloneSessionStartInfo,
|
|
3635
3735
|
createFinalStatsEvents,
|
|
3736
|
+
createIterationCompletionEvents,
|
|
3737
|
+
createIterationObservationEvents,
|
|
3636
3738
|
createRealtimeStatsEvents,
|
|
3637
3739
|
createRunResultEvents,
|
|
3638
3740
|
createReportingEvent,
|
|
@@ -3649,6 +3751,7 @@ exports.__loadstrikeTestExports = {
|
|
|
3649
3751
|
normalizeStringMap,
|
|
3650
3752
|
optionNumber,
|
|
3651
3753
|
optionString,
|
|
3754
|
+
portalEventPayload,
|
|
3652
3755
|
pickBooleanValue,
|
|
3653
3756
|
pickRecordValue,
|
|
3654
3757
|
postWithTimeout,
|
package/dist/cjs/transports.js
CHANGED
|
@@ -638,14 +638,10 @@ class WebSocketEndpointDefinitionModel extends TrafficEndpointDefinitionModel {
|
|
|
638
638
|
parsed = new URL(url);
|
|
639
639
|
}
|
|
640
640
|
catch {
|
|
641
|
-
|
|
642
|
-
// nosemgrep: vulnerability-tools.semgrep-rules.javascript.lang.security.detect-insecure-websocket
|
|
643
|
-
throw new Error("Url must be an absolute ws:// or wss:// URI.");
|
|
641
|
+
throw new Error("Url must be an absolute WebSocket URI using the ws or wss scheme.");
|
|
644
642
|
}
|
|
645
643
|
if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
|
|
646
|
-
|
|
647
|
-
// nosemgrep: vulnerability-tools.semgrep-rules.javascript.lang.security.detect-insecure-websocket
|
|
648
|
-
throw new Error("Url must be an absolute ws:// or wss:// URI.");
|
|
644
|
+
throw new Error("Url must be an absolute WebSocket URI using the ws or wss scheme.");
|
|
649
645
|
}
|
|
650
646
|
if (this.ConnectTimeoutSeconds <= 0) {
|
|
651
647
|
throw new RangeError("ConnectTimeout must be greater than zero.");
|
|
@@ -3175,14 +3171,10 @@ function validateWebSocketEndpoint(endpoint, mode) {
|
|
|
3175
3171
|
parsed = new URL(url);
|
|
3176
3172
|
}
|
|
3177
3173
|
catch {
|
|
3178
|
-
|
|
3179
|
-
// nosemgrep: vulnerability-tools.semgrep-rules.javascript.lang.security.detect-insecure-websocket
|
|
3180
|
-
throw new Error("Url must be an absolute ws:// or wss:// URI.");
|
|
3174
|
+
throw new Error("Url must be an absolute WebSocket URI using the ws or wss scheme.");
|
|
3181
3175
|
}
|
|
3182
3176
|
if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
|
|
3183
|
-
|
|
3184
|
-
// nosemgrep: vulnerability-tools.semgrep-rules.javascript.lang.security.detect-insecure-websocket
|
|
3185
|
-
throw new Error("Url must be an absolute ws:// or wss:// URI.");
|
|
3177
|
+
throw new Error("Url must be an absolute WebSocket URI using the ws or wss scheme.");
|
|
3186
3178
|
}
|
|
3187
3179
|
const connectMs = optionNumber(options, "ConnectTimeoutMs", "connectTimeoutMs");
|
|
3188
3180
|
const connectSeconds = optionNumber(options, "ConnectTimeoutSeconds", "connectTimeoutSeconds", "ConnectTimeout", "connectTimeout");
|
|
@@ -3612,7 +3604,13 @@ function extractJsonPath(value, arrayPath) {
|
|
|
3612
3604
|
if (!normalized) {
|
|
3613
3605
|
return value;
|
|
3614
3606
|
}
|
|
3615
|
-
|
|
3607
|
+
let tokens;
|
|
3608
|
+
try {
|
|
3609
|
+
tokens = safeJsonPathSegments(normalized.replace(/\[(\d+)\]/g, ".$1"));
|
|
3610
|
+
}
|
|
3611
|
+
catch {
|
|
3612
|
+
return null;
|
|
3613
|
+
}
|
|
3616
3614
|
for (const token of tokens) {
|
|
3617
3615
|
if (current == null) {
|
|
3618
3616
|
return null;
|
|
@@ -3622,13 +3620,17 @@ function extractJsonPath(value, arrayPath) {
|
|
|
3622
3620
|
if (!Number.isInteger(index) || index < 0 || index >= current.length) {
|
|
3623
3621
|
return null;
|
|
3624
3622
|
}
|
|
3625
|
-
current = current
|
|
3623
|
+
current = current.at(index);
|
|
3626
3624
|
continue;
|
|
3627
3625
|
}
|
|
3628
|
-
if (!isRecord(current)
|
|
3626
|
+
if (!isRecord(current)) {
|
|
3629
3627
|
return null;
|
|
3630
3628
|
}
|
|
3631
|
-
|
|
3629
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, token);
|
|
3630
|
+
if (!descriptor || !("value" in descriptor)) {
|
|
3631
|
+
return null;
|
|
3632
|
+
}
|
|
3633
|
+
current = descriptor.value;
|
|
3632
3634
|
}
|
|
3633
3635
|
return current;
|
|
3634
3636
|
}
|
|
@@ -3645,7 +3647,7 @@ function parseMaybeJson(value) {
|
|
|
3645
3647
|
}
|
|
3646
3648
|
}
|
|
3647
3649
|
function headersToRecord(value) {
|
|
3648
|
-
const headers =
|
|
3650
|
+
const headers = Object.create(null);
|
|
3649
3651
|
value.forEach((headerValue, key) => {
|
|
3650
3652
|
headers[key] = headerValue;
|
|
3651
3653
|
});
|
|
@@ -3999,11 +4001,22 @@ function extractTrackingValue(payload, selector) {
|
|
|
3999
4001
|
return null;
|
|
4000
4002
|
}
|
|
4001
4003
|
let current = body;
|
|
4002
|
-
|
|
4004
|
+
const path = selector.slice("json:".length).trim().replace(/^\$\./, "");
|
|
4005
|
+
let segments;
|
|
4006
|
+
try {
|
|
4007
|
+
segments = safeJsonPathSegments(path);
|
|
4008
|
+
}
|
|
4009
|
+
catch {
|
|
4010
|
+
return null;
|
|
4011
|
+
}
|
|
4012
|
+
for (const segment of segments) {
|
|
4003
4013
|
if (!isRecord(current)) {
|
|
4004
4014
|
return null;
|
|
4005
4015
|
}
|
|
4006
|
-
current
|
|
4016
|
+
if (!Object.prototype.hasOwnProperty.call(current, segment)) {
|
|
4017
|
+
return null;
|
|
4018
|
+
}
|
|
4019
|
+
current = readOwnJsonProperty(current, segment);
|
|
4007
4020
|
}
|
|
4008
4021
|
return current == null ? null : String(current);
|
|
4009
4022
|
}
|
|
@@ -4034,23 +4047,61 @@ function injectTrackingValue(payload, selector, value) {
|
|
|
4034
4047
|
}
|
|
4035
4048
|
function setJsonBodyValue(body, path, value) {
|
|
4036
4049
|
const target = parseBodyObject(body) ?? {};
|
|
4037
|
-
const clone =
|
|
4038
|
-
const segments = path
|
|
4050
|
+
const clone = cloneJsonRecord(target);
|
|
4051
|
+
const segments = safeJsonPathSegments(path);
|
|
4039
4052
|
if (!segments.length) {
|
|
4040
4053
|
return clone;
|
|
4041
4054
|
}
|
|
4042
4055
|
let current = clone;
|
|
4043
4056
|
for (let i = 0; i < segments.length - 1; i += 1) {
|
|
4044
4057
|
const segment = segments[i];
|
|
4045
|
-
|
|
4058
|
+
let next = readOwnJsonProperty(current, segment);
|
|
4046
4059
|
if (!isRecord(next)) {
|
|
4047
|
-
|
|
4060
|
+
next = {};
|
|
4061
|
+
defineJsonProperty(current, segment, next);
|
|
4048
4062
|
}
|
|
4049
|
-
current =
|
|
4063
|
+
current = next;
|
|
4050
4064
|
}
|
|
4051
|
-
current
|
|
4065
|
+
defineJsonProperty(current, segments[segments.length - 1], value);
|
|
4052
4066
|
return clone;
|
|
4053
4067
|
}
|
|
4068
|
+
const FORBIDDEN_JSON_PATH_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
|
|
4069
|
+
function safeJsonPathSegments(path) {
|
|
4070
|
+
const segments = path.split(".").filter(Boolean);
|
|
4071
|
+
const forbidden = segments.find((segment) => FORBIDDEN_JSON_PATH_SEGMENTS.has(segment));
|
|
4072
|
+
if (forbidden) {
|
|
4073
|
+
throw new Error(`Tracking selector contains forbidden JSON path segment '${forbidden}'.`);
|
|
4074
|
+
}
|
|
4075
|
+
return segments;
|
|
4076
|
+
}
|
|
4077
|
+
function defineJsonProperty(target, key, value) {
|
|
4078
|
+
Object.defineProperty(target, key, {
|
|
4079
|
+
configurable: true,
|
|
4080
|
+
enumerable: true,
|
|
4081
|
+
value,
|
|
4082
|
+
writable: true
|
|
4083
|
+
});
|
|
4084
|
+
}
|
|
4085
|
+
function readOwnJsonProperty(target, key) {
|
|
4086
|
+
const descriptor = Object.getOwnPropertyDescriptor(target, key);
|
|
4087
|
+
return descriptor && "value" in descriptor ? descriptor.value : undefined;
|
|
4088
|
+
}
|
|
4089
|
+
function cloneJsonRecord(source) {
|
|
4090
|
+
const target = {};
|
|
4091
|
+
for (const [key, value] of Object.entries(source)) {
|
|
4092
|
+
defineJsonProperty(target, key, cloneJsonValue(value));
|
|
4093
|
+
}
|
|
4094
|
+
return target;
|
|
4095
|
+
}
|
|
4096
|
+
function cloneJsonValue(value) {
|
|
4097
|
+
if (Array.isArray(value)) {
|
|
4098
|
+
return value.map((entry) => cloneJsonValue(entry));
|
|
4099
|
+
}
|
|
4100
|
+
if (isRecord(value)) {
|
|
4101
|
+
return cloneJsonRecord(value);
|
|
4102
|
+
}
|
|
4103
|
+
return value;
|
|
4104
|
+
}
|
|
4054
4105
|
function parseBodyObject(body) {
|
|
4055
4106
|
if (body instanceof Uint8Array) {
|
|
4056
4107
|
return parseBodyObject(new TextDecoder().decode(body));
|
|
@@ -4659,8 +4710,10 @@ exports.__loadstrikeTestExports = {
|
|
|
4659
4710
|
createNatsHeaders,
|
|
4660
4711
|
createRedisStreamPayload,
|
|
4661
4712
|
deserializeBrokerPayloadBody,
|
|
4713
|
+
extractJsonPath,
|
|
4662
4714
|
extractTrackingValue,
|
|
4663
4715
|
fromKafkaHeaders,
|
|
4716
|
+
headersToRecord,
|
|
4664
4717
|
headerValue,
|
|
4665
4718
|
attachDotNetTrackingPayloadAliases,
|
|
4666
4719
|
attachPayloadHelpers,
|