@loadstrike/loadstrike-sdk 1.0.22801 → 1.0.23201

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/esm/index.js CHANGED
@@ -2,5 +2,5 @@ export { LoadStrikeAutopilot, LoadStrikeAutopilotResult } from "./autopilot.js";
2
2
  export { LoadStrikeAutopilotReadiness } from "./autopilot-contracts.js";
3
3
  export { CrossPlatformScenarioConfigurator, ScenarioTrackingExtensions, LoadStrikeContext, LoadStrikePluginData, LoadStrikePluginDataTable, LoadStrikeNodeType, LoadStrikeReportFormat, LoadStrikeResponse, LoadStrikeLogLevel, LoadStrikeScenarioOperation, LoadStrikeOperationType, LoadStrikeRunner, LoadStrikeScenario, LoadStrikeSimulation, CrossPlatformTrackingConfiguration, LoadStrikeMetric, LoadStrikeCounter, LoadStrikeGauge, LoadStrikeStep, LoadStrikeThreshold } from "./runtime.js";
4
4
  export { CorrelationStoreConfiguration, CrossPlatformTrackingRuntime, InMemoryCorrelationStore, RedisCorrelationStoreOptions, RedisCorrelationStore, TrackingPayloadBuilder, TrackingFieldSelector } from "./correlation.js";
5
- export { EndpointAdapterFactory, LOADSTRIKE_TRACE_ID_HEADER, LOADSTRIKE_TRACE_ID_TRACKING_FIELD, TrafficEndpointDefinition, HttpEndpointDefinition, KafkaEndpointDefinition, KafkaSaslOptions, RabbitMqEndpointDefinition, NatsEndpointDefinition, RedisStreamsEndpointDefinition, AzureEventHubsEndpointDefinition, DelegateStreamEndpointDefinition, PushDiffusionEndpointDefinition, HttpOAuth2ClientCredentialsOptions, HttpAuthOptions } from "./transports.js";
6
- export { DatadogReportingSink, DatadogReportingSinkOptions, GrafanaLokiReportingSink, GrafanaLokiReportingSinkOptions, InfluxDbReportingSink, InfluxDbReportingSinkOptions, OtelCollectorReportingSink, OtelCollectorReportingSinkOptions, SplunkReportingSink, SplunkReportingSinkOptions, TimescaleDbReportingSink, TimescaleDbReportingSinkOptions } from "./sinks.js";
5
+ export { LOADSTRIKE_TRACE_ID_HEADER, LOADSTRIKE_TRACE_ID_TRACKING_FIELD, TrafficEndpointDefinition, HttpEndpointDefinition, KafkaEndpointDefinition, KafkaSaslOptions, RabbitMqEndpointDefinition, NatsEndpointDefinition, RedisStreamsEndpointDefinition, AzureEventHubsEndpointDefinition, SqsEndpointDefinition, DelegateStreamEndpointDefinition, PushDiffusionEndpointDefinition, HttpOAuth2ClientCredentialsOptions, HttpAuthOptions } from "./transports.js";
6
+ export { DatadogReportingSink, DatadogReportingSinkOptions, GrafanaLokiReportingSink, GrafanaLokiReportingSinkOptions, InfluxDbReportingSink, InfluxDbReportingSinkOptions, OtelCollectorReportingSink, OtelCollectorReportingSinkOptions, PortalReportingSink, SplunkReportingSink, SplunkReportingSinkOptions, TimescaleDbReportingSink, TimescaleDbReportingSinkOptions } from "./sinks.js";
package/dist/esm/local.js CHANGED
@@ -5,7 +5,6 @@ import { createHash, createVerify, randomUUID } from "node:crypto";
5
5
  import { CorrelationStoreConfiguration, CrossPlatformTrackingRuntime, RedisCorrelationStoreOptions, RedisCorrelationStore, TrackingFieldSelector } from "./correlation.js";
6
6
  import { EndpointAdapterFactory, LOADSTRIKE_TRACE_ID_TRACKING_FIELD } from "./transports.js";
7
7
  const DEFAULT_LICENSING_API_BASE_URL = "https://licensing.loadstrike.com";
8
- const INTERNAL_BLACKBOX_LICENSING_API_BASE_URL_ENVIRONMENT_VARIABLE = "LOADSTRIKE_INTERNAL_BLACKBOX_API_BASE_URL";
9
8
  let developmentLicensingApiBaseUrlOverride;
10
9
  const BUILT_IN_WORKER_PLUGIN_NAMES = new Set([
11
10
  "loadstrike failed responses",
@@ -17,7 +16,8 @@ const SINK_FEATURE_BY_KIND = {
17
16
  grafanaloki: "extensions.reporting_sinks.grafana_loki",
18
17
  datadog: "extensions.reporting_sinks.datadog",
19
18
  splunk: "extensions.reporting_sinks.splunk",
20
- otelcollector: "extensions.reporting_sinks.otel_collector"
19
+ otelcollector: "extensions.reporting_sinks.otel_collector",
20
+ portal: "extensions.reporting_sinks.portal"
21
21
  };
22
22
  const TRACKING_FEATURE_BY_KIND = {
23
23
  http: "endpoint.http",
@@ -27,7 +27,8 @@ const TRACKING_FEATURE_BY_KIND = {
27
27
  pushdiffusion: "endpoint.push_diffusion",
28
28
  delegatestream: "endpoint.delegate_stream",
29
29
  nats: "endpoint.nats",
30
- redisstreams: "endpoint.redis_streams"
30
+ redisstreams: "endpoint.redis_streams",
31
+ sqs: "endpoint.sqs"
31
32
  };
32
33
  const CI_ENVIRONMENT_VARIABLES = [
33
34
  "GITHUB_ACTIONS",
@@ -57,6 +58,9 @@ export class LoadStrikeLocalClient {
57
58
  this.licensingApiBaseUrl = resolveLicensingApiBaseUrl();
58
59
  this.licenseValidationTimeoutMs = normalizeTimeoutMs(options.licenseValidationTimeoutMs);
59
60
  }
61
+ portalReportingIngestUrl() {
62
+ return `${this.licensingApiBaseUrl.replace(/\/+$/, "")}/api/v1/reporting/ingest`;
63
+ }
60
64
  async run(request) {
61
65
  const sanitized = sanitizeRequest(request);
62
66
  const licenseSession = await this.acquireLicenseLease(sanitized);
@@ -466,25 +470,8 @@ function normalizeLicensingApiBaseUrl(value) {
466
470
  const normalized = (value ?? "").trim();
467
471
  return normalized || DEFAULT_LICENSING_API_BASE_URL;
468
472
  }
469
- function resolveInternalBlackboxLicensingApiBaseUrlOverride(value) {
470
- const normalized = String(value ?? "").trim();
471
- if (!normalized) {
472
- return undefined;
473
- }
474
- try {
475
- const parsed = new URL(normalized);
476
- const host = parsed.hostname.replace(/^\[|\]$/g, "").toLowerCase();
477
- if (["http:", "https:"].includes(parsed.protocol) && ["127.0.0.1", "localhost", "::1"].includes(host)) {
478
- return normalized.replace(/\/+$/, "");
479
- }
480
- }
481
- catch {
482
- return undefined;
483
- }
484
- return undefined;
485
- }
486
473
  function resolveLicensingApiBaseUrl() {
487
- return normalizeLicensingApiBaseUrl(resolveInternalBlackboxLicensingApiBaseUrlOverride(process.env[INTERNAL_BLACKBOX_LICENSING_API_BASE_URL_ENVIRONMENT_VARIABLE]) ?? developmentLicensingApiBaseUrlOverride);
474
+ return normalizeLicensingApiBaseUrl(developmentLicensingApiBaseUrlOverride);
488
475
  }
489
476
  function setDevelopmentLicensingApiBaseUrlOverride(value) {
490
477
  developmentLicensingApiBaseUrlOverride = value;
@@ -523,6 +510,9 @@ function collectRequestedFeatures(request) {
523
510
  if (countCustomWorkerPlugins(context) > 0) {
524
511
  features.add("extensions.worker_plugins.custom");
525
512
  }
513
+ if (asList(pickValue(context, "RuntimePolicies", "runtimePolicies")).length > 0) {
514
+ features.add("policy.runtime_controls");
515
+ }
526
516
  const reportingSinks = asList(pickValue(context, "ReportingSinks", "reportingSinks"));
527
517
  let hasCustomSink = false;
528
518
  for (const sink of reportingSinks) {
@@ -818,159 +808,171 @@ async function evaluateScenarioOutcome(scenario, requestCount, context) {
818
808
  validateTrackingConfiguration(tracking, sourceEndpoint, destinationEndpoint);
819
809
  const sourceAdapter = EndpointAdapterFactory.create(sourceEndpoint);
820
810
  const destinationAdapter = destinationEndpoint ? EndpointAdapterFactory.create(destinationEndpoint) : null;
821
- const correlationTimeoutOverride = pickValue(tracking, "CorrelationTimeoutMs", "correlationTimeoutMs");
822
- const correlationTimeoutSeconds = pickValue(tracking, "CorrelationTimeoutSeconds", "correlationTimeoutSeconds", "CorrelationTimeout");
823
- const timeoutMs = correlationTimeoutOverride != null && String(correlationTimeoutOverride).trim() !== ""
824
- ? asInt(correlationTimeoutOverride)
825
- : Math.trunc((correlationTimeoutSeconds == null || String(correlationTimeoutSeconds).trim() === ""
826
- ? 30
827
- : asNumber(correlationTimeoutSeconds)) * 1000);
828
- const timeoutCountsAsFailure = toBoolean(pickValue(tracking, "TimeoutCountsAsFailure", "timeoutCountsAsFailure"), true);
829
- const correlationStore = mapCorrelationStore(tracking, buildTrackingRunNamespace(stringOrDefault(pickValue(context, "SessionId", "sessionId"), "session"), stringOrDefault(pickValue(scenario, "Name", "name"), "scenario"), sourceEndpoint.name, destinationEndpoint?.name));
830
- const timeoutSweepIntervalOverride = pickValue(tracking, "TimeoutSweepIntervalMs", "timeoutSweepIntervalMs");
831
- const timeoutSweepIntervalSeconds = pickValue(tracking, "TimeoutSweepIntervalSeconds", "timeoutSweepIntervalSeconds");
832
- const timeoutSweepIntervalMs = timeoutSweepIntervalOverride != null && String(timeoutSweepIntervalOverride).trim() !== ""
833
- ? asInt(timeoutSweepIntervalOverride)
834
- : Math.trunc((timeoutSweepIntervalSeconds == null || String(timeoutSweepIntervalSeconds).trim() === ""
835
- ? 1
836
- : asNumber(timeoutSweepIntervalSeconds)) * 1000);
837
- const timeoutBatchSizeValue = pickValue(tracking, "TimeoutBatchSize", "timeoutBatchSize");
838
- const timeoutBatchSize = timeoutBatchSizeValue == null || String(timeoutBatchSizeValue).trim() === ""
839
- ? 200
840
- : asInt(timeoutBatchSizeValue);
841
- const runtime = new CrossPlatformTrackingRuntime({
842
- sourceTrackingField: sourceEndpoint.trackingField,
843
- destinationTrackingField: destinationEndpoint?.trackingField,
844
- destinationGatherByField: destinationEndpoint?.gatherByField,
845
- trackingFieldValueCaseSensitive: toBoolean(pickValue(tracking, "TrackingFieldValueCaseSensitive", "trackingFieldValueCaseSensitive"), true),
846
- gatherByFieldValueCaseSensitive: toBoolean(pickValue(tracking, "GatherByFieldValueCaseSensitive", "gatherByFieldValueCaseSensitive"), true),
847
- correlationTimeoutMs: timeoutMs,
848
- timeoutCountsAsFailure,
849
- store: correlationStore ?? undefined
850
- });
851
- const runMode = stringOrDefault(pickValue(tracking, "RunMode", "runMode"), "GenerateAndCorrelate").trim().toLowerCase();
852
- const sourceOnlyMode = !destinationEndpoint;
853
- const restartOnFail = toBoolean(pickValue(scenario, "RestartIterationOnFail", "restartIterationOnFail"), false);
854
- const restartMaxAttempts = Math.max(asInt(pickValue(context, "RestartIterationMaxAttempts", "restartIterationMaxAttempts")), 0);
855
- const maxFailCount = Math.max(asInt(pickValue(scenario, "MaxFailCount", "maxFailCount")), 0);
856
- const scenarioCompletionTimeoutSeconds = Math.max(asNumber(pickValue(context, "ScenarioCompletionTimeoutSeconds", "scenarioCompletionTimeoutSeconds")), 0);
857
- const scenarioDeadlineMs = scenarioCompletionTimeoutSeconds > 0
858
- ? Date.now() + Math.trunc(scenarioCompletionTimeoutSeconds * 1000)
859
- : Number.POSITIVE_INFINITY;
860
- let okCount = 0;
861
- let failCount = 0;
862
- let nowMs = Date.now();
863
- let processedIterations = 0;
864
- let lastSweepAtMs = nowMs;
865
- for (let i = 0; i < requestCount; i += 1) {
866
- if (Date.now() > scenarioDeadlineMs) {
867
- break;
868
- }
869
- let iterationComplete = false;
870
- let attempts = 0;
871
- const maxAttempts = 1 + (restartOnFail ? restartMaxAttempts : 0);
872
- while (!iterationComplete) {
873
- attempts += 1;
811
+ let correlationStore = null;
812
+ try {
813
+ await sourceAdapter.initialize?.();
814
+ await destinationAdapter?.initialize?.();
815
+ const correlationTimeoutOverride = pickValue(tracking, "CorrelationTimeoutMs", "correlationTimeoutMs");
816
+ const correlationTimeoutSeconds = pickValue(tracking, "CorrelationTimeoutSeconds", "correlationTimeoutSeconds", "CorrelationTimeout");
817
+ const timeoutMs = correlationTimeoutOverride != null && String(correlationTimeoutOverride).trim() !== ""
818
+ ? asInt(correlationTimeoutOverride)
819
+ : Math.trunc((correlationTimeoutSeconds == null || String(correlationTimeoutSeconds).trim() === ""
820
+ ? 30
821
+ : asNumber(correlationTimeoutSeconds)) * 1000);
822
+ const timeoutCountsAsFailure = toBoolean(pickValue(tracking, "TimeoutCountsAsFailure", "timeoutCountsAsFailure"), true);
823
+ correlationStore = mapCorrelationStore(tracking, buildTrackingRunNamespace(stringOrDefault(pickValue(context, "SessionId", "sessionId"), "session"), stringOrDefault(pickValue(scenario, "Name", "name"), "scenario"), sourceEndpoint.name, destinationEndpoint?.name));
824
+ const timeoutSweepIntervalOverride = pickValue(tracking, "TimeoutSweepIntervalMs", "timeoutSweepIntervalMs");
825
+ const timeoutSweepIntervalSeconds = pickValue(tracking, "TimeoutSweepIntervalSeconds", "timeoutSweepIntervalSeconds");
826
+ const timeoutSweepIntervalMs = timeoutSweepIntervalOverride != null && String(timeoutSweepIntervalOverride).trim() !== ""
827
+ ? asInt(timeoutSweepIntervalOverride)
828
+ : Math.trunc((timeoutSweepIntervalSeconds == null || String(timeoutSweepIntervalSeconds).trim() === ""
829
+ ? 1
830
+ : asNumber(timeoutSweepIntervalSeconds)) * 1000);
831
+ const timeoutBatchSizeValue = pickValue(tracking, "TimeoutBatchSize", "timeoutBatchSize");
832
+ const timeoutBatchSize = timeoutBatchSizeValue == null || String(timeoutBatchSizeValue).trim() === ""
833
+ ? 200
834
+ : asInt(timeoutBatchSizeValue);
835
+ const runtime = new CrossPlatformTrackingRuntime({
836
+ sourceTrackingField: sourceEndpoint.trackingField,
837
+ destinationTrackingField: destinationEndpoint?.trackingField,
838
+ destinationGatherByField: destinationEndpoint?.gatherByField,
839
+ trackingFieldValueCaseSensitive: toBoolean(pickValue(tracking, "TrackingFieldValueCaseSensitive", "trackingFieldValueCaseSensitive"), true),
840
+ gatherByFieldValueCaseSensitive: toBoolean(pickValue(tracking, "GatherByFieldValueCaseSensitive", "gatherByFieldValueCaseSensitive"), true),
841
+ correlationTimeoutMs: timeoutMs,
842
+ timeoutCountsAsFailure,
843
+ store: correlationStore ?? undefined
844
+ });
845
+ const runMode = stringOrDefault(pickValue(tracking, "RunMode", "runMode"), "GenerateAndCorrelate").trim().toLowerCase();
846
+ const sourceOnlyMode = !destinationEndpoint;
847
+ const restartOnFail = toBoolean(pickValue(scenario, "RestartIterationOnFail", "restartIterationOnFail"), false);
848
+ const restartMaxAttempts = Math.max(asInt(pickValue(context, "RestartIterationMaxAttempts", "restartIterationMaxAttempts")), 0);
849
+ const maxFailCount = Math.max(asInt(pickValue(scenario, "MaxFailCount", "maxFailCount")), 0);
850
+ const scenarioCompletionTimeoutSeconds = Math.max(asNumber(pickValue(context, "ScenarioCompletionTimeoutSeconds", "scenarioCompletionTimeoutSeconds")), 0);
851
+ const scenarioDeadlineMs = scenarioCompletionTimeoutSeconds > 0
852
+ ? Date.now() + Math.trunc(scenarioCompletionTimeoutSeconds * 1000)
853
+ : Number.POSITIVE_INFINITY;
854
+ let okCount = 0;
855
+ let failCount = 0;
856
+ let nowMs = Date.now();
857
+ let processedIterations = 0;
858
+ let lastSweepAtMs = nowMs;
859
+ for (let i = 0; i < requestCount; i += 1) {
874
860
  if (Date.now() > scenarioDeadlineMs) {
875
- iterationComplete = true;
876
861
  break;
877
862
  }
878
- let sourcePayload = runMode === "correlateexistingtraffic"
879
- ? await sourceAdapter.consume()
880
- : await sourceAdapter.produce();
881
- sourcePayload = normalizePayload(sourcePayload, sourceEndpoint, i);
882
- const sourceTrackingId = sourceOnlyMode
883
- ? readTrackingId(sourcePayload, sourceEndpoint.trackingField)
884
- : await runtime.onSourceProduced(sourcePayload, nowMs);
885
- if (!sourceTrackingId) {
886
- if (restartOnFail && attempts < maxAttempts) {
863
+ let iterationComplete = false;
864
+ let attempts = 0;
865
+ const maxAttempts = 1 + (restartOnFail ? restartMaxAttempts : 0);
866
+ while (!iterationComplete) {
867
+ attempts += 1;
868
+ if (Date.now() > scenarioDeadlineMs) {
869
+ iterationComplete = true;
870
+ break;
871
+ }
872
+ let sourcePayload = runMode === "correlateexistingtraffic"
873
+ ? await sourceAdapter.consume()
874
+ : await sourceAdapter.produce();
875
+ sourcePayload = normalizePayload(sourcePayload, sourceEndpoint, i);
876
+ const sourceTrackingId = sourceOnlyMode
877
+ ? readTrackingId(sourcePayload, sourceEndpoint.trackingField)
878
+ : await runtime.onSourceProduced(sourcePayload, nowMs);
879
+ if (!sourceTrackingId) {
880
+ if (restartOnFail && attempts < maxAttempts) {
881
+ nowMs += 1;
882
+ continue;
883
+ }
884
+ failCount += 1;
885
+ iterationComplete = true;
887
886
  nowMs += 1;
888
- continue;
887
+ break;
889
888
  }
890
- failCount += 1;
891
- iterationComplete = true;
892
- nowMs += 1;
893
- break;
894
- }
895
- if (sourceOnlyMode || !destinationAdapter || !destinationEndpoint) {
896
- okCount += 1;
897
- iterationComplete = true;
898
- nowMs += 1;
899
- break;
900
- }
901
- let destinationPayload = await destinationAdapter.consume();
902
- destinationPayload = normalizePayload(destinationPayload, destinationEndpoint, i);
903
- let matched = await runtime.onDestinationConsumed(destinationPayload, nowMs + 1);
904
- const correlationDeadlineMs = nowMs + timeoutMs;
905
- while (!matched && Date.now() <= scenarioDeadlineMs && nowMs < correlationDeadlineMs) {
906
- const remainingTimeoutMs = correlationDeadlineMs - nowMs;
907
- if (remainingTimeoutMs <= 0) {
889
+ if (sourceOnlyMode || !destinationAdapter || !destinationEndpoint) {
890
+ okCount += 1;
891
+ iterationComplete = true;
892
+ nowMs += 1;
908
893
  break;
909
894
  }
910
- await sleep(Math.min(destinationEndpoint.pollIntervalMs ?? 250, remainingTimeoutMs));
911
- destinationPayload = normalizePayload(await destinationAdapter.consume(), destinationEndpoint, i);
912
- matched = await runtime.onDestinationConsumed(destinationPayload, nowMs + 1);
895
+ let destinationPayload = await destinationAdapter.consume();
896
+ destinationPayload = normalizePayload(destinationPayload, destinationEndpoint, i);
897
+ let matched = await runtime.onDestinationConsumed(destinationPayload, nowMs + 1);
898
+ const correlationDeadlineMs = nowMs + timeoutMs;
899
+ while (!matched && Date.now() <= scenarioDeadlineMs && nowMs < correlationDeadlineMs) {
900
+ const remainingTimeoutMs = correlationDeadlineMs - nowMs;
901
+ if (remainingTimeoutMs <= 0) {
902
+ break;
903
+ }
904
+ await sleep(Math.min(destinationEndpoint.pollIntervalMs ?? 250, remainingTimeoutMs));
905
+ destinationPayload = normalizePayload(await destinationAdapter.consume(), destinationEndpoint, i);
906
+ matched = await runtime.onDestinationConsumed(destinationPayload, nowMs + 1);
907
+ if (matched) {
908
+ break;
909
+ }
910
+ nowMs += Math.max(destinationEndpoint.pollIntervalMs ?? 250, 1);
911
+ }
913
912
  if (matched) {
914
- break;
913
+ okCount += 1;
914
+ iterationComplete = true;
915
915
  }
916
- nowMs += Math.max(destinationEndpoint.pollIntervalMs ?? 250, 1);
917
- }
918
- if (matched) {
919
- okCount += 1;
920
- iterationComplete = true;
921
- }
922
- else if (restartOnFail && attempts < maxAttempts) {
923
- nowMs += 2;
924
- continue;
925
- }
926
- else {
927
- await runtime.sweepTimeouts(nowMs + timeoutMs + 1, timeoutBatchSize || undefined);
928
- failCount += 1;
929
- iterationComplete = true;
930
- }
931
- nowMs += 2;
932
- }
933
- processedIterations += 1;
934
- if (timeoutSweepIntervalMs > 0 && nowMs - lastSweepAtMs >= timeoutSweepIntervalMs) {
935
- const swept = await runtime.sweepTimeouts(nowMs, timeoutBatchSize || undefined);
936
- if (swept > 0) {
937
- if (timeoutCountsAsFailure) {
938
- failCount += swept;
916
+ else if (restartOnFail && attempts < maxAttempts) {
917
+ nowMs += 2;
918
+ continue;
939
919
  }
940
920
  else {
941
- okCount += swept;
921
+ await runtime.sweepTimeouts(nowMs + timeoutMs + 1, timeoutBatchSize || undefined);
922
+ failCount += 1;
923
+ iterationComplete = true;
924
+ }
925
+ nowMs += 2;
926
+ }
927
+ processedIterations += 1;
928
+ if (timeoutSweepIntervalMs > 0 && nowMs - lastSweepAtMs >= timeoutSweepIntervalMs) {
929
+ const swept = await runtime.sweepTimeouts(nowMs, timeoutBatchSize || undefined);
930
+ if (swept > 0) {
931
+ if (timeoutCountsAsFailure) {
932
+ failCount += swept;
933
+ }
934
+ else {
935
+ okCount += swept;
936
+ }
942
937
  }
938
+ lastSweepAtMs = nowMs;
939
+ }
940
+ if (maxFailCount > 0 && failCount >= maxFailCount) {
941
+ break;
943
942
  }
944
- lastSweepAtMs = nowMs;
945
- }
946
- if (maxFailCount > 0 && failCount >= maxFailCount) {
947
- break;
948
943
  }
949
- }
950
- let expired = 0;
951
- let sweptChunk = 0;
952
- do {
953
- sweptChunk = await runtime.sweepTimeouts(nowMs + timeoutMs + 1, timeoutBatchSize || undefined);
954
- expired += sweptChunk;
955
- } while (timeoutBatchSize > 0 && sweptChunk >= timeoutBatchSize);
956
- if (expired > 0) {
957
- if (timeoutCountsAsFailure) {
958
- failCount += expired;
944
+ let expired = 0;
945
+ let sweptChunk = 0;
946
+ do {
947
+ sweptChunk = await runtime.sweepTimeouts(nowMs + timeoutMs + 1, timeoutBatchSize || undefined);
948
+ expired += sweptChunk;
949
+ } while (timeoutBatchSize > 0 && sweptChunk >= timeoutBatchSize);
950
+ if (expired > 0) {
951
+ if (timeoutCountsAsFailure) {
952
+ failCount += expired;
953
+ }
954
+ else {
955
+ okCount += expired;
956
+ }
959
957
  }
960
- else {
961
- okCount += expired;
958
+ if (processedIterations < requestCount && Date.now() > scenarioDeadlineMs) {
959
+ const timedOutIterations = requestCount - processedIterations;
960
+ if (timeoutCountsAsFailure) {
961
+ failCount += timedOutIterations;
962
+ }
963
+ else {
964
+ okCount += timedOutIterations;
965
+ }
962
966
  }
967
+ return { requestCount: processedIterations, okCount, failCount };
963
968
  }
964
- if (processedIterations < requestCount && Date.now() > scenarioDeadlineMs) {
965
- const timedOutIterations = requestCount - processedIterations;
966
- if (timeoutCountsAsFailure) {
967
- failCount += timedOutIterations;
968
- }
969
- else {
970
- okCount += timedOutIterations;
971
- }
969
+ finally {
970
+ await Promise.allSettled([
971
+ sourceAdapter.dispose?.(),
972
+ destinationAdapter?.dispose?.(),
973
+ correlationStore?.close?.()
974
+ ].filter((value) => Boolean(value)));
972
975
  }
973
- return { requestCount: processedIterations, okCount, failCount };
974
976
  }
975
977
  function validateTrackingConfiguration(tracking, sourceEndpoint, destinationEndpoint) {
976
978
  if (sourceEndpoint.gatherByField?.trim()) {
@@ -7,6 +7,7 @@ import { DistributedClusterAgent, DistributedClusterCoordinator } from "./cluste
7
7
  import { CorrelationStoreConfiguration, CrossPlatformTrackingRuntime, RedisCorrelationStore, RedisCorrelationStoreOptions, TrackingFieldSelector } from "./correlation.js";
8
8
  import { EndpointAdapterFactory, LOADSTRIKE_TRACE_ID_TRACKING_FIELD } from "./transports.js";
9
9
  import { buildDotnetCsvReport, buildDotnetHtmlReport, buildDotnetMarkdownReport, buildDotnetTxtReport } from "./reporting.js";
10
+ import { PortalReportingSink } from "./sinks.js";
10
11
  export const LoadStrikeNodeType = {
11
12
  SingleNode: "SingleNode",
12
13
  Coordinator: "Coordinator",
@@ -1378,6 +1379,16 @@ export class LoadStrikeContext {
1378
1379
  validateNamedReportingSinks(sinks);
1379
1380
  return this.mergeValues({ ReportingSinks: sinks });
1380
1381
  }
1382
+ withPortalReporting() {
1383
+ return this.WithPortalReporting();
1384
+ }
1385
+ WithPortalReporting() {
1386
+ const sinks = [...(this.values.ReportingSinks ?? [])];
1387
+ if (!sinks.some((sink, index) => resolveSinkName(sink, index).trim().toLowerCase() === "portal")) {
1388
+ sinks.push(new PortalReportingSink());
1389
+ }
1390
+ return this.mergeValues({ ReportingSinks: sinks });
1391
+ }
1381
1392
  /**
1382
1393
  * Registers runtime policies for the run.
1383
1394
  * Use this when scenario selection or step execution should obey policy callbacks.
@@ -2118,6 +2129,9 @@ export class LoadStrikeRunner {
2118
2129
  static WithReportingSinks(context, ...sinks) {
2119
2130
  return context.WithReportingSinks(...sinks);
2120
2131
  }
2132
+ static WithPortalReporting(context) {
2133
+ return context.WithPortalReporting();
2134
+ }
2121
2135
  /**
2122
2136
  * Registers runtime policies for the run.
2123
2137
  * Use this when scenario selection or step execution should obey policy callbacks.
@@ -2346,6 +2360,29 @@ export class LoadStrikeRunner {
2346
2360
  WithReportingInterval(intervalSeconds) {
2347
2361
  return this.withReportingInterval(intervalSeconds);
2348
2362
  }
2363
+ withReportingSinks(...sinks) {
2364
+ if (!sinks.length) {
2365
+ throw new Error("At least one reporting sink should be provided.");
2366
+ }
2367
+ if (sinks.some((sink) => sink == null)) {
2368
+ throw new Error("Reporting sink collection cannot contain null values.");
2369
+ }
2370
+ validateNamedReportingSinks(sinks);
2371
+ return this.configure({ reportingSinks: sinks });
2372
+ }
2373
+ WithReportingSinks(...sinks) {
2374
+ return this.withReportingSinks(...sinks);
2375
+ }
2376
+ withPortalReporting() {
2377
+ const sinks = [...(this.options.reportingSinks ?? [])];
2378
+ if (!sinks.some((sink, index) => resolveSinkName(sink, index).trim().toLowerCase() === "portal")) {
2379
+ sinks.push(new PortalReportingSink());
2380
+ }
2381
+ return this.configure({ reportingSinks: sinks });
2382
+ }
2383
+ WithPortalReporting() {
2384
+ return this.withPortalReporting();
2385
+ }
2349
2386
  /**
2350
2387
  * Sets the timeout for cluster command round-trips.
2351
2388
  * Use this when distributed control messages need a tighter or looser deadline.
@@ -2519,6 +2556,7 @@ export class LoadStrikeRunner {
2519
2556
  testInfo,
2520
2557
  getNodeInfo: () => attachNodeInfoAliases({ ...nodeInfo })
2521
2558
  };
2559
+ attachPortalReportingSession(sinkSession, sessionInfo, licenseClient, licenseSession);
2522
2560
  attachSessionStartInfoAliases(sessionInfo);
2523
2561
  nodeInfo.currentOperation = "Init";
2524
2562
  for (const plugin of plugins) {
@@ -4052,10 +4090,23 @@ function attachSessionStartInfoAliases(session) {
4052
4090
  attachAliasMap(session, {
4053
4091
  StartedUtc: "startedUtc",
4054
4092
  ScenarioNames: "scenarioNames",
4055
- Scenarios: "scenarios"
4093
+ Scenarios: "scenarios",
4094
+ RunToken: "runToken",
4095
+ PortalReportingIngestUrl: "portalReportingIngestUrl"
4056
4096
  });
4057
4097
  return session;
4058
4098
  }
4099
+ function attachPortalReportingSession(sinkSession, sessionInfo, licenseClient, licenseSession) {
4100
+ const runToken = stringValueOrDefault(licenseSession?.runToken, "").trim();
4101
+ if (!runToken || !licenseClient) {
4102
+ return;
4103
+ }
4104
+ const ingestUrl = licenseClient.portalReportingIngestUrl();
4105
+ sinkSession.runToken = runToken;
4106
+ sinkSession.portalReportingIngestUrl = ingestUrl;
4107
+ sessionInfo.runToken = runToken;
4108
+ sessionInfo.portalReportingIngestUrl = ingestUrl;
4109
+ }
4059
4110
  function attachScenarioInitContextAliases(context) {
4060
4111
  context.nodeInfo = attachNodeInfoAliases(context.nodeInfo);
4061
4112
  context.testInfo = attachTestInfoAliases(context.testInfo);
@@ -6606,6 +6657,7 @@ function mapRuntimeTrackingEndpointSpec(spec, useLoadStrikeTraceIdHeader = false
6606
6657
  nats: asTrackingRecord(pickTrackingValue(spec, "Nats", "nats")),
6607
6658
  redisStreams: asTrackingRecord(pickTrackingValue(spec, "RedisStreams", "redisStreams")),
6608
6659
  azureEventHubs: asTrackingRecord(pickTrackingValue(spec, "AzureEventHubs", "azureEventHubs")),
6660
+ sqs: asTrackingRecord(pickTrackingValue(spec, "Sqs", "sqs")),
6609
6661
  pushDiffusion: asTrackingRecord(pickTrackingValue(spec, "PushDiffusion", "pushDiffusion")),
6610
6662
  delegate: typeof delegateProduce === "function"
6611
6663
  || typeof delegateConsume === "function"