@loadstrike/loadstrike-sdk 1.0.31601 → 1.0.33601

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.
Files changed (33) hide show
  1. package/README.md +14 -2
  2. package/dist/cjs/internal/prometheus-remote-write.js +37 -0
  3. package/dist/cjs/internal/reporting-sink-http-error.js +17 -0
  4. package/dist/cjs/internal/vendor-metric-payloads.js +390 -0
  5. package/dist/cjs/iteration-observations.js +24 -8
  6. package/dist/cjs/local.js +48 -63
  7. package/dist/cjs/reporting-containment.js +242 -0
  8. package/dist/cjs/runtime.js +83 -3
  9. package/dist/cjs/sinks.js +1337 -38
  10. package/dist/cjs/transports.js +1339 -151
  11. package/dist/esm/internal/prometheus-remote-write.js +31 -0
  12. package/dist/esm/internal/reporting-sink-http-error.js +13 -0
  13. package/dist/esm/internal/vendor-metric-payloads.js +382 -0
  14. package/dist/esm/iteration-observations.js +24 -8
  15. package/dist/esm/local.js +49 -64
  16. package/dist/esm/reporting-containment.js +238 -0
  17. package/dist/esm/runtime.js +85 -5
  18. package/dist/esm/sinks.js +1334 -35
  19. package/dist/esm/transports.js +1335 -151
  20. package/dist/types/contracts.d.ts +1 -0
  21. package/dist/types/index.d.ts +1 -1
  22. package/dist/types/internal/prometheus-remote-write.d.ts +2 -0
  23. package/dist/types/internal/reporting-sink-http-error.d.ts +6 -0
  24. package/dist/types/internal/vendor-metric-payloads.d.ts +48 -0
  25. package/dist/types/local.d.ts +0 -6
  26. package/dist/types/reporting-containment.d.ts +2 -0
  27. package/dist/types/runtime.d.ts +1 -0
  28. package/dist/types/sinks.d.ts +134 -17
  29. package/dist/types/transports.d.ts +2 -0
  30. package/package.json +9 -3
  31. package/dist/cjs/internal-build.js +0 -4
  32. package/dist/esm/internal-build.js +0 -1
  33. package/dist/types/internal-build.d.ts +0 -1
@@ -0,0 +1,31 @@
1
+ import protobuf from "protobufjs";
2
+ import { prepareProtocolMetricPoints } from "./vendor-metric-payloads.js";
3
+ const PrometheusLabel = new protobuf.Type("Label")
4
+ .add(new protobuf.Field("name", 1, "string"))
5
+ .add(new protobuf.Field("value", 2, "string"));
6
+ const PrometheusSample = new protobuf.Type("Sample")
7
+ .add(new protobuf.Field("value", 1, "double"))
8
+ .add(new protobuf.Field("timestamp", 2, "int64"));
9
+ const PrometheusTimeSeries = new protobuf.Type("TimeSeries")
10
+ .add(new protobuf.Field("labels", 1, "Label", "repeated"))
11
+ .add(new protobuf.Field("samples", 2, "Sample", "repeated"))
12
+ .add(PrometheusLabel)
13
+ .add(PrometheusSample);
14
+ const PrometheusWriteRequest = new protobuf.Type("WriteRequest")
15
+ .add(new protobuf.Field("timeseries", 1, "TimeSeries", "repeated"))
16
+ .add(PrometheusTimeSeries);
17
+ export function encodePrometheusRemoteWrite(points, staticLabels = {}) {
18
+ const prepared = prepareProtocolMetricPoints("prometheus", points, staticLabels);
19
+ const timeseries = prepared.map((point) => {
20
+ const labels = [["__name__", point.metricName], ...point.attributes];
21
+ labels.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
22
+ return {
23
+ labels: labels.map(([name, value]) => ({ name, value })),
24
+ samples: [{ value: point.value, timestamp: point.occurredUtc.getTime() }]
25
+ };
26
+ });
27
+ return {
28
+ contentType: "application/x-protobuf",
29
+ body: PrometheusWriteRequest.encode({ timeseries }).finish()
30
+ };
31
+ }
@@ -0,0 +1,13 @@
1
+ export class ReportingSinkHttpError extends Error {
2
+ constructor(sinkName, status, statusText, requestId) {
3
+ const normalizedStatus = Number.isFinite(status) ? Math.max(Math.trunc(status), 0) : 0;
4
+ super(`${sinkName} write failed with HTTP status ${normalizedStatus}`
5
+ + (statusText ? ` ${statusText}` : "")
6
+ + (requestId ? ` (requestId=${requestId})` : "")
7
+ + ".");
8
+ this.name = "ReportingSinkHttpError";
9
+ this.status = normalizedStatus;
10
+ this.statusText = statusText;
11
+ this.requestId = requestId;
12
+ }
13
+ }
@@ -0,0 +1,382 @@
1
+ const MAXIMUM_METRIC_NAME_BYTES = 200;
2
+ const MAXIMUM_ATTRIBUTE_KEY_BYTES = 64;
3
+ const MAXIMUM_ATTRIBUTE_VALUE_BYTES = 256;
4
+ const MAXIMUM_DYNATRACE_ATTRIBUTE_VALUE_BYTES = 255;
5
+ const MAXIMUM_ATTRIBUTES = 20;
6
+ const MAXIMUM_ABSOLUTE_VALUE = 1e100;
7
+ const MAXIMUM_SIGNED_INT64 = 9223372036854775807n;
8
+ const RESERVED_ATTRIBUTES = new Set(["scenario", "status", "run_id"]);
9
+ export function createCloudWatchMetricData(namespace, points, staticDimensions = {}) {
10
+ const normalizedNamespace = normalizeCloudWatchNamespace(namespace);
11
+ const prepared = prepareProtocolMetricPoints("cloudwatch", points, staticDimensions);
12
+ return {
13
+ Namespace: normalizedNamespace,
14
+ MetricData: prepared.map((point) => {
15
+ const unit = cloudWatchUnit(point.metricKind, point.unitOfMeasure);
16
+ return {
17
+ MetricName: point.metricName,
18
+ Dimensions: point.attributes.map(([Name, Value]) => ({ Name, Value })),
19
+ Value: point.value,
20
+ ...(unit ? { Unit: unit } : {}),
21
+ Timestamp: new Date(point.occurredUtc.getTime())
22
+ };
23
+ })
24
+ };
25
+ }
26
+ export function encodeDynatraceMetrics(points, staticDimensions = {}) {
27
+ const prepared = prepareProtocolMetricPoints("dynatrace", points, staticDimensions);
28
+ return encodeDynatracePreparedMetrics(prepared);
29
+ }
30
+ export function encodeDynatraceMetricBatches(points, staticDimensions = {}, maximumBodyBytesExclusive = 1000000) {
31
+ const prepared = prepareProtocolMetricPoints("dynatrace", points, staticDimensions);
32
+ return encodeTextBatches("dynatrace", "text/plain; charset=utf-8", prepared.map((point) => dynatraceMetricLine(point)), "", "", "\n", maximumBodyBytesExclusive);
33
+ }
34
+ export function encodeNewRelicMetricBatch(points, staticAttributes = {}) {
35
+ const prepared = prepareProtocolMetricPoints("newrelic", points, staticAttributes);
36
+ return encodeNewRelicPreparedMetrics(prepared);
37
+ }
38
+ export function encodeNewRelicMetricBatches(points, staticAttributes = {}, maximumBodyBytesExclusive = 1000000) {
39
+ const prepared = prepareProtocolMetricPoints("newrelic", points, staticAttributes);
40
+ return encodeTextBatches("newrelic", "application/json", prepared.map((point) => newRelicMetricJson(point)), '[{"metrics":[', "]}]", ",", maximumBodyBytesExclusive);
41
+ }
42
+ export function prepareProtocolMetricPoints(sink, points, staticAttributes = {}) {
43
+ if (!Array.isArray(points) || points.length === 0) {
44
+ throw protocolError(sink, "metric batch");
45
+ }
46
+ if (!isPlainRecord(staticAttributes)) {
47
+ throw protocolError(sink, "attribute collection");
48
+ }
49
+ // Validate and copy every point before any protocol bytes or DTOs are emitted.
50
+ const prepared = [];
51
+ for (let index = 0; index < points.length; index += 1) {
52
+ prepared.push(prepareProtocolMetricPoint(sink, points[index], staticAttributes));
53
+ }
54
+ return prepared;
55
+ }
56
+ function prepareProtocolMetricPoint(sink, point, staticAttributes) {
57
+ if (!point || typeof point !== "object") {
58
+ throw protocolError(sink, "metric point");
59
+ }
60
+ const metricKind = normalizeMetricKind(sink, point.metricKind);
61
+ if (!(point.occurredUtc instanceof Date) || !Number.isFinite(point.occurredUtc.getTime())) {
62
+ throw protocolError(sink, "timestamp");
63
+ }
64
+ if (!Number.isFinite(point.value) || point.value < -MAXIMUM_ABSOLUTE_VALUE || point.value > MAXIMUM_ABSOLUTE_VALUE) {
65
+ throw protocolError(sink, "value");
66
+ }
67
+ const intervalMs = metricKind === "count"
68
+ ? normalizePositiveInt64(sink, point.intervalMs)
69
+ : undefined;
70
+ if (metricKind === "gauge" && point.intervalMs !== undefined) {
71
+ throw protocolError(sink, "interval");
72
+ }
73
+ const rawUnitOfMeasure = point.unitOfMeasure;
74
+ let unitOfMeasure;
75
+ if (rawUnitOfMeasure !== undefined && rawUnitOfMeasure !== null) {
76
+ if (typeof rawUnitOfMeasure !== "string" || !isWellFormedUnicode(rawUnitOfMeasure)) {
77
+ throw protocolError(sink, "unit of measure");
78
+ }
79
+ unitOfMeasure = rawUnitOfMeasure;
80
+ }
81
+ const metricName = normalizeMetricName(sink, point.metricName);
82
+ const authoritative = [
83
+ ["scenario", validateAuthoritativeValue(sink, "scenario", point.scenario)],
84
+ ["status", validateAuthoritativeValue(sink, "status", point.status)]
85
+ ];
86
+ if (point.runId !== undefined) {
87
+ authoritative.push(["run_id", validateAuthoritativeValue(sink, "run_id", point.runId)]);
88
+ }
89
+ const emptyValuesAllowed = sink === "prometheus" || sink === "newrelic";
90
+ const normalizedStatic = new Map();
91
+ for (const [rawKey, rawValue] of Object.entries(staticAttributes)) {
92
+ const key = normalizeAttributeKey(sink, rawKey);
93
+ if (RESERVED_ATTRIBUTES.has(key)) {
94
+ throw protocolError(sink, key);
95
+ }
96
+ if (sink === "prometheus" && key === "__name__") {
97
+ throw protocolError(sink, "attribute key");
98
+ }
99
+ if (normalizedStatic.has(key)) {
100
+ throw protocolError(sink, "attribute key collision");
101
+ }
102
+ if (typeof rawValue !== "string" || !isWellFormedUnicode(rawValue) ||
103
+ (!emptyValuesAllowed && rawValue.length === 0) ||
104
+ utf8Length(rawValue) > maximumAttributeValueBytes(sink) ||
105
+ (sink === "dynatrace" && /[\r\n]/.test(rawValue)) ||
106
+ (sink === "cloudwatch" && !isCloudWatchDimensionValue(rawValue))) {
107
+ throw protocolError(sink, "attribute value");
108
+ }
109
+ normalizedStatic.set(key, rawValue);
110
+ }
111
+ if (authoritative.length + normalizedStatic.size > MAXIMUM_ATTRIBUTES) {
112
+ throw protocolError(sink, "attribute count");
113
+ }
114
+ const attributes = [...authoritative, ...normalizedStatic.entries()]
115
+ .sort(([left], [right]) => compareOrdinal(left, right));
116
+ return {
117
+ metricName,
118
+ metricKind,
119
+ occurredUtc: new Date(point.occurredUtc.getTime()),
120
+ value: point.value,
121
+ unitOfMeasure,
122
+ intervalMs,
123
+ attributes
124
+ };
125
+ }
126
+ function encodeDynatracePreparedMetrics(prepared) {
127
+ return {
128
+ contentType: "text/plain; charset=utf-8",
129
+ body: new TextEncoder().encode(prepared.map((point) => dynatraceMetricLine(point)).join("\n"))
130
+ };
131
+ }
132
+ function dynatraceMetricLine(point) {
133
+ const dimensions = point.attributes
134
+ .map(([key, value]) => `${key}="${escapeDynatraceDimensionValue(value)}"`)
135
+ .join(",");
136
+ const identity = dimensions.length > 0 ? `${point.metricName},${dimensions}` : point.metricName;
137
+ const payload = point.metricKind === "count"
138
+ ? `count,delta=${formatFiniteNumber(point.value)}`
139
+ : `gauge,${formatFiniteNumber(point.value)}`;
140
+ return `${identity} ${payload} ${point.occurredUtc.getTime()}`;
141
+ }
142
+ function encodeNewRelicPreparedMetrics(prepared) {
143
+ return {
144
+ contentType: "application/json",
145
+ body: new TextEncoder().encode(`[{"metrics":[${prepared.map((point) => newRelicMetricJson(point)).join(",")}]}]`)
146
+ };
147
+ }
148
+ function newRelicMetricJson(point) {
149
+ const properties = [
150
+ `"name":${JSON.stringify(point.metricName)}`,
151
+ `"type":${JSON.stringify(point.metricKind)}`,
152
+ `"value":${JSON.stringify(point.value)}`,
153
+ `"timestamp":${point.occurredUtc.getTime()}`
154
+ ];
155
+ if (point.metricKind === "count") {
156
+ if (point.intervalMs === undefined) {
157
+ throw protocolError("newrelic", "interval");
158
+ }
159
+ properties.push(`"interval.ms":${point.intervalMs.toString()}`);
160
+ }
161
+ properties.push(`"attributes":${JSON.stringify(Object.fromEntries(point.attributes))}`);
162
+ return `{${properties.join(",")}}`;
163
+ }
164
+ function encodeTextBatches(sink, contentType, items, prefix, suffix, separator, maximumBodyBytesExclusive) {
165
+ if (!Number.isSafeInteger(maximumBodyBytesExclusive) || maximumBodyBytesExclusive <= 0) {
166
+ throw protocolError(sink, "payload size limit");
167
+ }
168
+ const prefixBytes = utf8Length(prefix);
169
+ const suffixBytes = utf8Length(suffix);
170
+ const separatorBytes = utf8Length(separator);
171
+ const batches = [];
172
+ let current = [];
173
+ let currentBytes = prefixBytes + suffixBytes;
174
+ for (const item of items) {
175
+ const itemBytes = utf8Length(item);
176
+ if (prefixBytes + itemBytes + suffixBytes >= maximumBodyBytesExclusive) {
177
+ throw protocolError(sink, "payload size");
178
+ }
179
+ const candidateBytes = currentBytes + (current.length > 0 ? separatorBytes : 0) + itemBytes;
180
+ if (candidateBytes >= maximumBodyBytesExclusive) {
181
+ batches.push(current);
182
+ current = [item];
183
+ currentBytes = prefixBytes + itemBytes + suffixBytes;
184
+ }
185
+ else {
186
+ current.push(item);
187
+ currentBytes = candidateBytes;
188
+ }
189
+ }
190
+ if (current.length > 0) {
191
+ batches.push(current);
192
+ }
193
+ const encoder = new TextEncoder();
194
+ return batches.map((batch) => ({
195
+ contentType,
196
+ body: encoder.encode(`${prefix}${batch.join(separator)}${suffix}`)
197
+ }));
198
+ }
199
+ function normalizePositiveInt64(sink, value) {
200
+ let normalized;
201
+ if (typeof value === "bigint") {
202
+ normalized = value;
203
+ }
204
+ else if (typeof value === "number" && Number.isSafeInteger(value)) {
205
+ normalized = BigInt(value);
206
+ }
207
+ else {
208
+ throw protocolError(sink, "interval");
209
+ }
210
+ if (normalized <= 0n || normalized > MAXIMUM_SIGNED_INT64) {
211
+ throw protocolError(sink, "interval");
212
+ }
213
+ return normalized;
214
+ }
215
+ function normalizeMetricName(sink, value) {
216
+ if (typeof value !== "string" || !isWellFormedUnicode(value) || value.trim().length === 0) {
217
+ throw protocolError(sink, "metric name");
218
+ }
219
+ let normalized = sink === "prometheus"
220
+ ? normalizeAsciiIdentifier(value.trim(), true)
221
+ : sink === "dynatrace"
222
+ ? normalizeAsciiIdentifier(value.trim(), false)
223
+ : value.trim();
224
+ if (sink === "dynatrace" && utf8Length(normalized) < 3) {
225
+ normalized = `ls_${normalized}`;
226
+ }
227
+ if (normalized.length === 0 || utf8Length(normalized) > MAXIMUM_METRIC_NAME_BYTES) {
228
+ throw protocolError(sink, "metric name");
229
+ }
230
+ return normalized;
231
+ }
232
+ function normalizeMetricKind(sink, value) {
233
+ if (typeof value !== "string" || !isWellFormedUnicode(value)) {
234
+ throw protocolError(sink, "metric kind");
235
+ }
236
+ let normalized = "";
237
+ for (const character of value.trim()) {
238
+ normalized += character >= "A" && character <= "Z"
239
+ ? String.fromCharCode(character.charCodeAt(0) + 0x20)
240
+ : character;
241
+ }
242
+ if (normalized !== "count" && normalized !== "gauge") {
243
+ throw protocolError(sink, "metric kind");
244
+ }
245
+ return normalized;
246
+ }
247
+ function normalizeAttributeKey(sink, value) {
248
+ if (typeof value !== "string" || !isWellFormedUnicode(value) || value.trim().length === 0) {
249
+ throw protocolError(sink, "attribute key");
250
+ }
251
+ if (sink === "cloudwatch" && !isPrintableASCII(value)) {
252
+ throw protocolError(sink, "attribute key");
253
+ }
254
+ let normalized;
255
+ if (sink === "prometheus")
256
+ normalized = normalizeAsciiIdentifier(value.trim(), false);
257
+ else if (sink === "dynatrace")
258
+ normalized = normalizeAsciiIdentifier(value.trim(), false).toLowerCase();
259
+ else if (sink === "newrelic")
260
+ normalized = normalizeNewRelicAttributeKey(value.trim());
261
+ else
262
+ normalized = value.trim();
263
+ if (normalized.length === 0 || utf8Length(normalized) > MAXIMUM_ATTRIBUTE_KEY_BYTES) {
264
+ throw protocolError(sink, "attribute key");
265
+ }
266
+ if (sink === "cloudwatch" && !isCloudWatchDimensionName(normalized)) {
267
+ throw protocolError(sink, "attribute key");
268
+ }
269
+ return normalized;
270
+ }
271
+ function normalizeAsciiIdentifier(value, allowColon) {
272
+ let normalized = "";
273
+ for (const character of value) {
274
+ normalized += /[A-Za-z0-9_]/.test(character) || (allowColon && character === ":")
275
+ ? character
276
+ : "_";
277
+ }
278
+ if (!/^[A-Za-z_]/.test(normalized) && !(allowColon && normalized.startsWith(":"))) {
279
+ normalized = `_${normalized}`;
280
+ }
281
+ return normalized;
282
+ }
283
+ function validateAuthoritativeValue(sink, field, value) {
284
+ if (typeof value !== "string" || !isWellFormedUnicode(value) ||
285
+ value.trim().length === 0 || utf8Length(value) > maximumAttributeValueBytes(sink) ||
286
+ (sink === "dynatrace" && /[\r\n]/.test(value)) ||
287
+ (sink === "cloudwatch" && !isCloudWatchDimensionValue(value))) {
288
+ throw protocolError(sink, field);
289
+ }
290
+ return value;
291
+ }
292
+ function normalizeCloudWatchNamespace(value) {
293
+ if (typeof value !== "string" || !isWellFormedUnicode(value)) {
294
+ throw protocolError("cloudwatch", "namespace");
295
+ }
296
+ const normalized = value.trim();
297
+ if (utf8Length(normalized) < 1 || utf8Length(normalized) > 100 || normalized.startsWith(":")) {
298
+ throw protocolError("cloudwatch", "namespace");
299
+ }
300
+ return normalized;
301
+ }
302
+ function cloudWatchUnit(metricKind, unit) {
303
+ if (metricKind === "count")
304
+ return "Count";
305
+ switch (unit?.trim().toLowerCase()) {
306
+ case "count":
307
+ case "counts": return "Count";
308
+ case "ms":
309
+ case "millisecond":
310
+ case "milliseconds": return "Milliseconds";
311
+ case "byte":
312
+ case "bytes": return "Bytes";
313
+ case "s":
314
+ case "sec":
315
+ case "seconds":
316
+ case "second": return "Seconds";
317
+ default: return "None";
318
+ }
319
+ }
320
+ function escapeDynatraceDimensionValue(value) {
321
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
322
+ }
323
+ function formatFiniteNumber(value) {
324
+ return Object.is(value, -0) ? "0" : String(value);
325
+ }
326
+ function utf8Length(value) {
327
+ return new TextEncoder().encode(value).byteLength;
328
+ }
329
+ function isWellFormedUnicode(value) {
330
+ for (let index = 0; index < value.length; index += 1) {
331
+ const codeUnit = value.charCodeAt(index);
332
+ if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
333
+ if (index + 1 >= value.length)
334
+ return false;
335
+ const next = value.charCodeAt(index + 1);
336
+ if (next < 0xdc00 || next > 0xdfff)
337
+ return false;
338
+ index += 1;
339
+ }
340
+ else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
341
+ return false;
342
+ }
343
+ }
344
+ return true;
345
+ }
346
+ function isPlainRecord(value) {
347
+ if (typeof value !== "object" || value === null || Array.isArray(value))
348
+ return false;
349
+ const prototype = Object.getPrototypeOf(value);
350
+ return prototype === Object.prototype || prototype === null;
351
+ }
352
+ function normalizeNewRelicAttributeKey(value) {
353
+ let normalized = "";
354
+ for (const character of value) {
355
+ normalized += /[A-Za-z0-9:._]/.test(character) ? character : "_";
356
+ }
357
+ return normalized;
358
+ }
359
+ function maximumAttributeValueBytes(sink) {
360
+ return sink === "dynatrace" ? MAXIMUM_DYNATRACE_ATTRIBUTE_VALUE_BYTES : MAXIMUM_ATTRIBUTE_VALUE_BYTES;
361
+ }
362
+ function isCloudWatchDimensionName(value) {
363
+ return !value.startsWith(":") && isPrintableASCII(value) && value.trim().length > 0;
364
+ }
365
+ function isCloudWatchDimensionValue(value) {
366
+ return isPrintableASCII(value) && value.trim().length > 0;
367
+ }
368
+ function isPrintableASCII(value) {
369
+ for (const character of value) {
370
+ const code = character.charCodeAt(0);
371
+ if (code < 0x20 || code > 0x7e)
372
+ return false;
373
+ }
374
+ return true;
375
+ }
376
+ function protocolError(sink, field) {
377
+ const displayName = sink === "newrelic" ? "New Relic" : sink === "cloudwatch" ? "CloudWatch" : sink;
378
+ return new Error(`${displayName} reporting metric has an invalid ${field}.`);
379
+ }
380
+ function compareOrdinal(left, right) {
381
+ return left < right ? -1 : left > right ? 1 : 0;
382
+ }
@@ -144,7 +144,7 @@ export class ProcessIterationObservationBuffer {
144
144
  }
145
145
  const processIterationObservationBuffer = new ProcessIterationObservationBuffer();
146
146
  class SinkDispatcher {
147
- constructor(target, depth, parallelism, retryCount, retryBackoffMs, logger, runId, resultOwnerId, onDropped) {
147
+ constructor(target, depth, parallelism, retryCount, retryBackoffMs, logger, runId, resultOwnerId, retryableErrorClassifier, onDropped) {
148
148
  this.target = target;
149
149
  this.depth = depth;
150
150
  this.parallelism = parallelism;
@@ -153,6 +153,7 @@ class SinkDispatcher {
153
153
  this.logger = logger;
154
154
  this.runId = runId;
155
155
  this.resultOwnerId = resultOwnerId;
156
+ this.retryableErrorClassifier = retryableErrorClassifier;
156
157
  this.onDropped = onDropped;
157
158
  this.queue = [];
158
159
  this.active = new Set();
@@ -265,12 +266,14 @@ class SinkDispatcher {
265
266
  return "success";
266
267
  }
267
268
  catch (error) {
268
- const exhausted = attempt >= maximumAttempts;
269
+ const retryable = this.isRetryableError(error);
270
+ const exhausted = attempt >= maximumAttempts || !retryable;
271
+ const reportedMaximumAttempts = retryable ? maximumAttempts : attempt;
269
272
  const nextDelayMs = exhausted
270
273
  ? 0
271
274
  : sinkRetryDelayMs(this.retryBackoffMs, attempt);
272
275
  logIterationObservationFailure(this.logger, exhausted ? "error" : "warn", {
273
- ...this.diagnosticContext(item, attempt, maximumAttempts),
276
+ ...this.diagnosticContext(item, attempt, reportedMaximumAttempts),
274
277
  nextDelayMs
275
278
  }, error);
276
279
  if (exhausted) {
@@ -284,6 +287,17 @@ class SinkDispatcher {
284
287
  }
285
288
  return "exhausted";
286
289
  }
290
+ isRetryableError(error) {
291
+ if (!this.retryableErrorClassifier) {
292
+ return true;
293
+ }
294
+ try {
295
+ return this.retryableErrorClassifier(error) !== false;
296
+ }
297
+ catch {
298
+ return true;
299
+ }
300
+ }
287
301
  diagnosticContext(item, attempt, maximumAttempts) {
288
302
  return {
289
303
  sinkName: this.target.name,
@@ -408,7 +422,7 @@ export class IterationObservationReporter {
408
422
  this.recordWarning("sink_iteration_batches_unsupported", sink.name, "", -1, 1n, "The reporting sink does not support raw iteration batches.");
409
423
  return false;
410
424
  })
411
- .map((target) => new SinkDispatcher(target, this.settings.sinkQueueDepth, this.settings.sinkParallelism, normalizeSinkRetryCount(options.sinkRetryCount), normalizeSinkRetryBackoffMs(options.sinkRetryBackoffMs), options.logger, options.runId, options.resultOwnerId, (code, sinkName, observations) => this.recordSinkDrop(code, sinkName, observations)));
425
+ .map((target) => new SinkDispatcher(target, this.settings.sinkQueueDepth, this.settings.sinkParallelism, normalizeSinkRetryCount(options.sinkRetryCount), normalizeSinkRetryBackoffMs(options.sinkRetryBackoffMs), options.logger, options.runId, options.resultOwnerId, target.retryableErrorClassifier, (code, sinkName, observations) => this.recordSinkDrop(code, sinkName, observations)));
412
426
  if (this.dispatchers.length > 0) {
413
427
  this.buffer.register(this.streamKey, this.settings.maxBufferBytes);
414
428
  }
@@ -486,7 +500,7 @@ export class IterationObservationReporter {
486
500
  && dispatcher.droppedCount === 0n
487
501
  && dispatcher.deliveredCount === this.captured;
488
502
  const completion = this.createCompletion(dispatcher, sinkReportingComplete);
489
- const outcome = await this.completeWithRetries(complete, completion, deadline, dispatcher.target.name);
503
+ const outcome = await this.completeWithRetries(dispatcher, complete, completion, deadline, dispatcher.target.name);
490
504
  if (outcome === "success") {
491
505
  return true;
492
506
  }
@@ -522,7 +536,7 @@ export class IterationObservationReporter {
522
536
  };
523
537
  return { ...this.finalResult };
524
538
  }
525
- async completeWithRetries(complete, completion, deadline, sinkName) {
539
+ async completeWithRetries(dispatcher, complete, completion, deadline, sinkName) {
526
540
  const retryCount = normalizeSinkRetryCount(this.options.sinkRetryCount);
527
541
  const backoffMs = normalizeSinkRetryBackoffMs(this.options.sinkRetryBackoffMs);
528
542
  const maximumAttempts = retryCount + 1;
@@ -561,7 +575,9 @@ export class IterationObservationReporter {
561
575
  if (outcome.kind === "timeout") {
562
576
  return "timeout";
563
577
  }
564
- const exhausted = attempt >= maximumAttempts;
578
+ const retryable = dispatcher.isRetryableError(outcome.error);
579
+ const exhausted = attempt >= maximumAttempts || !retryable;
580
+ const reportedMaximumAttempts = retryable ? maximumAttempts : attempt;
565
581
  const nextDelayMs = exhausted ? 0 : sinkRetryDelayMs(backoffMs, attempt);
566
582
  logIterationObservationFailure(this.options.logger, exhausted ? "error" : "warn", {
567
583
  sinkName,
@@ -570,7 +586,7 @@ export class IterationObservationReporter {
570
586
  resultOwnerId: this.options.resultOwnerId,
571
587
  observationCount: 0,
572
588
  attempt,
573
- maximumAttempts,
589
+ maximumAttempts: reportedMaximumAttempts,
574
590
  nextDelayMs
575
591
  }, outcome.error);
576
592
  if (exhausted) {