@oxygen-agent/cli 1.861.1 → 1.879.0
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 +1 -1
- package/dist/column-run-notices.d.ts +10 -0
- package/dist/column-run-notices.js +22 -0
- package/dist/index.js +187 -73
- package/dist/local-custom-http-column.js +12 -37
- package/node_modules/@oxygen/formula/dist/formula-functions.js +10 -24
- package/node_modules/@oxygen/shared/dist/cell-format.js +23 -2
- package/node_modules/@oxygen/shared/dist/column-output-fields.d.ts +122 -0
- package/node_modules/@oxygen/shared/dist/column-output-fields.js +459 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/index.js +1 -0
- package/node_modules/@oxygen/shared/dist/json-path.d.ts +109 -0
- package/node_modules/@oxygen/shared/dist/json-path.js +177 -0
- package/node_modules/@oxygen/shared/dist/log-collapse.d.ts +6 -3
- package/node_modules/@oxygen/shared/dist/log-collapse.js +6 -3
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +13 -0
- package/node_modules/@oxygen/shared/dist/research-output-contract.d.ts +53 -0
- package/node_modules/@oxygen/shared/dist/research-output-contract.js +196 -0
- package/node_modules/@oxygen/shared/dist/sending-seats.d.ts +17 -1
- package/node_modules/@oxygen/shared/dist/sending-seats.js +17 -1
- package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +47 -0
- package/node_modules/@oxygen/shared/dist/sequence-failures.js +301 -0
- package/node_modules/@oxygen/shared/dist/telemetry.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/telemetry.js +119 -2
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/package.json +15 -0
- package/package.json +1 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export declare const SEQUENCE_FAILURE_SCHEMA_VERSION = 1;
|
|
2
|
+
export declare const SEQUENCE_FAILURE_RETRY_AFTER_MAX_MS: number;
|
|
3
|
+
export declare const SEQUENCE_FAILURE_CLASSES: readonly ["capacity", "recipient", "authorization_scope", "funding", "validation", "provider", "internal", "effect_unknown", "unknown"];
|
|
4
|
+
export type SequenceFailureClass = (typeof SEQUENCE_FAILURE_CLASSES)[number];
|
|
5
|
+
export type SequenceFailureEffectOutcome = "not_applied" | "applied" | "unknown";
|
|
6
|
+
export type SequenceFailureEnvelope = {
|
|
7
|
+
failure_schema_version: typeof SEQUENCE_FAILURE_SCHEMA_VERSION;
|
|
8
|
+
failure_class: SequenceFailureClass;
|
|
9
|
+
code: string;
|
|
10
|
+
message: string;
|
|
11
|
+
provider?: string;
|
|
12
|
+
operation?: string;
|
|
13
|
+
provider_subtype?: string;
|
|
14
|
+
http_status?: number;
|
|
15
|
+
retryable?: boolean;
|
|
16
|
+
effect_outcome?: SequenceFailureEffectOutcome;
|
|
17
|
+
retry_after_ms?: number;
|
|
18
|
+
hard_bounce?: boolean;
|
|
19
|
+
};
|
|
20
|
+
export type SequenceFailureOverrides = {
|
|
21
|
+
code?: string | null;
|
|
22
|
+
provider?: string | null;
|
|
23
|
+
operation?: string | null;
|
|
24
|
+
providerSubtype?: string | null;
|
|
25
|
+
httpStatus?: number | null;
|
|
26
|
+
retryable?: boolean | null;
|
|
27
|
+
effectOutcome?: SequenceFailureEffectOutcome | null;
|
|
28
|
+
retryAfterMs?: number | null;
|
|
29
|
+
hardBounce?: boolean | null;
|
|
30
|
+
/** Existing safe diagnostic fields to retain while adopting the envelope. */
|
|
31
|
+
legacyFields?: Record<string, unknown>;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Return one canonical, redacted Sequence action failure while retaining the
|
|
35
|
+
* legacy fields current readers may still consume. Canonical fields always win
|
|
36
|
+
* over legacy collisions, so future SQL can depend on one stable shape.
|
|
37
|
+
*
|
|
38
|
+
* This function classifies provenance only. It never decides whether an action
|
|
39
|
+
* retries, defers, fails, consumes credits, or advances an enrollment.
|
|
40
|
+
*/
|
|
41
|
+
export declare function serializeSequenceFailure(error: unknown, overrides?: SequenceFailureOverrides): Record<string, unknown> & SequenceFailureEnvelope;
|
|
42
|
+
/**
|
|
43
|
+
* Read both the versioned envelope and historical Sequence error shapes. A
|
|
44
|
+
* missing fact stays missing: in particular, this reader never manufactures a
|
|
45
|
+
* `not_applied` outcome from retryability, status, or error text.
|
|
46
|
+
*/
|
|
47
|
+
export declare function readSequenceFailure(error: unknown, overrides?: SequenceFailureOverrides): SequenceFailureEnvelope;
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { redactCustomerFacingDetails, redactCustomerFacingError, } from "./error-redaction.js";
|
|
2
|
+
import { isProviderFundingErrorCode, isProviderRateLimitErrorCode, } from "./provider-funding-errors.js";
|
|
3
|
+
export const SEQUENCE_FAILURE_SCHEMA_VERSION = 1;
|
|
4
|
+
export const SEQUENCE_FAILURE_RETRY_AFTER_MAX_MS = 24 * 60 * 60 * 1000;
|
|
5
|
+
export const SEQUENCE_FAILURE_CLASSES = [
|
|
6
|
+
"capacity",
|
|
7
|
+
"recipient",
|
|
8
|
+
"authorization_scope",
|
|
9
|
+
"funding",
|
|
10
|
+
"validation",
|
|
11
|
+
"provider",
|
|
12
|
+
"internal",
|
|
13
|
+
"effect_unknown",
|
|
14
|
+
"unknown",
|
|
15
|
+
];
|
|
16
|
+
const FAILURE_CLASS_SET = new Set(SEQUENCE_FAILURE_CLASSES);
|
|
17
|
+
const EFFECT_OUTCOME_SET = new Set([
|
|
18
|
+
"not_applied",
|
|
19
|
+
"applied",
|
|
20
|
+
"unknown",
|
|
21
|
+
]);
|
|
22
|
+
/**
|
|
23
|
+
* Return one canonical, redacted Sequence action failure while retaining the
|
|
24
|
+
* legacy fields current readers may still consume. Canonical fields always win
|
|
25
|
+
* over legacy collisions, so future SQL can depend on one stable shape.
|
|
26
|
+
*
|
|
27
|
+
* This function classifies provenance only. It never decides whether an action
|
|
28
|
+
* retries, defers, fails, consumes credits, or advances an enrollment.
|
|
29
|
+
*/
|
|
30
|
+
export function serializeSequenceFailure(error, overrides = {}) {
|
|
31
|
+
const base = redactRecord({
|
|
32
|
+
...legacyRecord(error),
|
|
33
|
+
...(overrides.legacyFields ?? {}),
|
|
34
|
+
});
|
|
35
|
+
const failure = readSequenceFailure(error, overrides);
|
|
36
|
+
const output = { ...base };
|
|
37
|
+
// A legacy payload may already carry malformed or unbounded values under the
|
|
38
|
+
// new keys. Remove them before projecting the validated canonical envelope.
|
|
39
|
+
for (const key of [
|
|
40
|
+
"failure_schema_version",
|
|
41
|
+
"failure_class",
|
|
42
|
+
"code",
|
|
43
|
+
"message",
|
|
44
|
+
"provider",
|
|
45
|
+
"operation",
|
|
46
|
+
"provider_subtype",
|
|
47
|
+
"http_status",
|
|
48
|
+
"retryable",
|
|
49
|
+
"effect_outcome",
|
|
50
|
+
"retry_after_ms",
|
|
51
|
+
"hard_bounce",
|
|
52
|
+
]) {
|
|
53
|
+
delete output[key];
|
|
54
|
+
}
|
|
55
|
+
Object.assign(output, failure, { error_message: failure.message });
|
|
56
|
+
return output;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Read both the versioned envelope and historical Sequence error shapes. A
|
|
60
|
+
* missing fact stays missing: in particular, this reader never manufactures a
|
|
61
|
+
* `not_applied` outcome from retryability, status, or error text.
|
|
62
|
+
*/
|
|
63
|
+
export function readSequenceFailure(error, overrides = {}) {
|
|
64
|
+
const records = failureRecords(error);
|
|
65
|
+
const sourceCode = overrides.code ?? firstString(records, ["code", "error_code", "errorCode"]);
|
|
66
|
+
const rawCode = redactCanonicalString(sourceCode) ?? "unknown_error";
|
|
67
|
+
const rawMessage = firstString(records, ["message", "error_message", "errorMessage"]) ??
|
|
68
|
+
(error instanceof Error ? error.message : null) ??
|
|
69
|
+
"Sequence action failed.";
|
|
70
|
+
const redacted = redactCustomerFacingError({
|
|
71
|
+
code: rawCode,
|
|
72
|
+
message: rawMessage,
|
|
73
|
+
error,
|
|
74
|
+
});
|
|
75
|
+
const provider = redactCanonicalString(overrides.provider ??
|
|
76
|
+
firstString(records, ["provider", "provider_id", "providerId"]));
|
|
77
|
+
const operation = redactCanonicalString(overrides.operation ??
|
|
78
|
+
firstString(records, [
|
|
79
|
+
"operation",
|
|
80
|
+
"provider_operation",
|
|
81
|
+
"providerOperation",
|
|
82
|
+
]));
|
|
83
|
+
const providerSubtype = redactCanonicalString(overrides.providerSubtype ??
|
|
84
|
+
firstString(records, [
|
|
85
|
+
"provider_subtype",
|
|
86
|
+
"source_code",
|
|
87
|
+
"sourceCode",
|
|
88
|
+
"provider_error_code",
|
|
89
|
+
]));
|
|
90
|
+
const httpStatus = normalizeHttpStatus(overrides.httpStatus ??
|
|
91
|
+
firstNumber(records, [
|
|
92
|
+
"http_status",
|
|
93
|
+
"httpStatus",
|
|
94
|
+
"status",
|
|
95
|
+
"status_code",
|
|
96
|
+
"statusCode",
|
|
97
|
+
"provider_status",
|
|
98
|
+
"providerStatus",
|
|
99
|
+
]));
|
|
100
|
+
const retryable = overrides.retryable ?? firstBoolean(records, ["retryable"]);
|
|
101
|
+
const effectOutcome = overrides.effectOutcome ?? readEffectOutcome(records);
|
|
102
|
+
const retryAfterMs = normalizeRetryAfterMs(overrides.retryAfterMs ?? readRetryAfterMs(records));
|
|
103
|
+
const hardBounce = overrides.hardBounce ??
|
|
104
|
+
firstBoolean(records, ["hard_bounce", "hardBounce"]);
|
|
105
|
+
const existingClass = firstString(records, ["failure_class"]);
|
|
106
|
+
const existingVersion = firstNumber(records, ["failure_schema_version"]);
|
|
107
|
+
const classified = classifySequenceFailure({
|
|
108
|
+
code: redacted.code,
|
|
109
|
+
providerSubtype,
|
|
110
|
+
provider,
|
|
111
|
+
httpStatus,
|
|
112
|
+
retryable,
|
|
113
|
+
effectOutcome,
|
|
114
|
+
hardBounce,
|
|
115
|
+
errorName: firstString(records, ["error_name", "name"]),
|
|
116
|
+
});
|
|
117
|
+
// A supported, versioned envelope owns its classification. Legacy payloads
|
|
118
|
+
// do not: their stray `failure_class` fields are mapped from the underlying
|
|
119
|
+
// facts. Effect-unknown remains the fail-safe exception even for a malformed
|
|
120
|
+
// v1 envelope, because it must never be made replay-safe by a stale class.
|
|
121
|
+
const failureClass = effectOutcome === "unknown"
|
|
122
|
+
? "effect_unknown"
|
|
123
|
+
: existingVersion === SEQUENCE_FAILURE_SCHEMA_VERSION &&
|
|
124
|
+
existingClass &&
|
|
125
|
+
FAILURE_CLASS_SET.has(existingClass)
|
|
126
|
+
? existingClass
|
|
127
|
+
: classified;
|
|
128
|
+
return {
|
|
129
|
+
failure_schema_version: SEQUENCE_FAILURE_SCHEMA_VERSION,
|
|
130
|
+
failure_class: failureClass,
|
|
131
|
+
code: redacted.code,
|
|
132
|
+
message: redacted.message,
|
|
133
|
+
...(provider ? { provider } : {}),
|
|
134
|
+
...(operation ? { operation } : {}),
|
|
135
|
+
...(providerSubtype ? { provider_subtype: providerSubtype } : {}),
|
|
136
|
+
...(httpStatus !== null ? { http_status: httpStatus } : {}),
|
|
137
|
+
...(retryable !== null ? { retryable } : {}),
|
|
138
|
+
...(effectOutcome ? { effect_outcome: effectOutcome } : {}),
|
|
139
|
+
...(retryAfterMs !== null ? { retry_after_ms: retryAfterMs } : {}),
|
|
140
|
+
...(hardBounce !== null ? { hard_bounce: hardBounce } : {}),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function classifySequenceFailure(input) {
|
|
144
|
+
const codes = [input.code, input.providerSubtype]
|
|
145
|
+
.filter(Boolean)
|
|
146
|
+
.join(" ")
|
|
147
|
+
.toLowerCase();
|
|
148
|
+
// Ambiguous effects win every other classification. A 429-shaped code or a
|
|
149
|
+
// provider name must never make a possibly-applied write safe to replay.
|
|
150
|
+
if (input.effectOutcome === "unknown" ||
|
|
151
|
+
/(?:effect|outcome)[_-]?unknown|ambiguous[_-]?(?:effect|write)/i.test(codes)) {
|
|
152
|
+
return "effect_unknown";
|
|
153
|
+
}
|
|
154
|
+
if ([input.code, input.providerSubtype].some((code) => isProviderFundingErrorCode(code)))
|
|
155
|
+
return "funding";
|
|
156
|
+
if (input.httpStatus === 429 ||
|
|
157
|
+
[input.code, input.providerSubtype].some((code) => isProviderRateLimitErrorCode(code))) {
|
|
158
|
+
return "capacity";
|
|
159
|
+
}
|
|
160
|
+
if (input.httpStatus === 401 ||
|
|
161
|
+
input.httpStatus === 403 ||
|
|
162
|
+
/auth|permission|scope|credential|forbidden|unauthori[sz]ed|not[_-]?connected|connection[_-]?(?:invalid|expired)/i.test(codes)) {
|
|
163
|
+
return "authorization_scope";
|
|
164
|
+
}
|
|
165
|
+
if (input.hardBounce === true ||
|
|
166
|
+
/recipient|hard[_-]?bounce|invalid[_-]?(?:profile|recipient|member)|lead[_-]?not[_-]?found|already[_-]?(?:connected|invited)|not[_-]?eligible|cannot[_-]?(?:message|invite)/i.test(codes)) {
|
|
167
|
+
return "recipient";
|
|
168
|
+
}
|
|
169
|
+
if (input.httpStatus === 400 ||
|
|
170
|
+
input.httpStatus === 422 ||
|
|
171
|
+
/validation|invalid[_-]?request|bad[_-]?request|malformed|required[_-]?field|unsupported[_-]?(?:input|value)/i.test(codes)) {
|
|
172
|
+
return "validation";
|
|
173
|
+
}
|
|
174
|
+
if ((input.httpStatus !== null && input.httpStatus >= 500) ||
|
|
175
|
+
(input.httpStatus === 404 && input.retryable === true) ||
|
|
176
|
+
input.provider !== null ||
|
|
177
|
+
/provider|upstream|tool[_-]?unavailable|network|timeout|temporar(?:y|ily)[_-]?unavailable/i.test(codes)) {
|
|
178
|
+
return "provider";
|
|
179
|
+
}
|
|
180
|
+
if (/^(?:TypeError|ReferenceError|RangeError|SyntaxError)$/i.test(input.errorName ?? "") ||
|
|
181
|
+
/internal|database|persistence|bookkeeping|invariant|concurrent[_-]?write/i.test(codes)) {
|
|
182
|
+
return "internal";
|
|
183
|
+
}
|
|
184
|
+
return "unknown";
|
|
185
|
+
}
|
|
186
|
+
function failureRecords(value) {
|
|
187
|
+
const direct = asRecord(value);
|
|
188
|
+
const nestedError = asRecord(direct?.error);
|
|
189
|
+
const directDetails = asRecord(direct?.details);
|
|
190
|
+
const nestedDetails = asRecord(nestedError?.details);
|
|
191
|
+
const body = asRecord(direct?.body);
|
|
192
|
+
const nestedBody = asRecord(nestedError?.body);
|
|
193
|
+
const providerError = asRecord(directDetails?.provider_error) ??
|
|
194
|
+
asRecord(nestedDetails?.provider_error);
|
|
195
|
+
return [
|
|
196
|
+
direct,
|
|
197
|
+
nestedError,
|
|
198
|
+
directDetails,
|
|
199
|
+
nestedDetails,
|
|
200
|
+
body,
|
|
201
|
+
nestedBody,
|
|
202
|
+
providerError,
|
|
203
|
+
].filter((entry) => entry !== null);
|
|
204
|
+
}
|
|
205
|
+
function legacyRecord(value) {
|
|
206
|
+
const direct = asRecord(value);
|
|
207
|
+
if (value instanceof Error) {
|
|
208
|
+
return {
|
|
209
|
+
...(direct ? Object.fromEntries(Object.entries(direct)) : {}),
|
|
210
|
+
error_name: value.name,
|
|
211
|
+
error_message: value.message,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (direct)
|
|
215
|
+
return direct;
|
|
216
|
+
return { error_id: "non_error", error_message: String(value) };
|
|
217
|
+
}
|
|
218
|
+
function redactRecord(value) {
|
|
219
|
+
const redacted = redactCustomerFacingDetails(value);
|
|
220
|
+
return asRecord(redacted) ?? {};
|
|
221
|
+
}
|
|
222
|
+
function redactCanonicalString(value) {
|
|
223
|
+
if (value === null)
|
|
224
|
+
return null;
|
|
225
|
+
const redacted = redactCustomerFacingDetails(value);
|
|
226
|
+
return typeof redacted === "string" && redacted.trim()
|
|
227
|
+
? redacted.trim()
|
|
228
|
+
: null;
|
|
229
|
+
}
|
|
230
|
+
function readEffectOutcome(records) {
|
|
231
|
+
const value = firstString(records, [
|
|
232
|
+
"effect_outcome",
|
|
233
|
+
"effectOutcome",
|
|
234
|
+
"outcome",
|
|
235
|
+
]);
|
|
236
|
+
if (value === "effect_unknown")
|
|
237
|
+
return "unknown";
|
|
238
|
+
return value && EFFECT_OUTCOME_SET.has(value)
|
|
239
|
+
? value
|
|
240
|
+
: null;
|
|
241
|
+
}
|
|
242
|
+
function readRetryAfterMs(records) {
|
|
243
|
+
const milliseconds = firstNumber(records, ["retry_after_ms", "retryAfterMs"]);
|
|
244
|
+
if (milliseconds !== null)
|
|
245
|
+
return milliseconds;
|
|
246
|
+
const seconds = firstNumber(records, [
|
|
247
|
+
"retry_after_seconds",
|
|
248
|
+
"retryAfterSeconds",
|
|
249
|
+
"retry_after",
|
|
250
|
+
]);
|
|
251
|
+
return seconds === null ? null : seconds * 1000;
|
|
252
|
+
}
|
|
253
|
+
function normalizeRetryAfterMs(value) {
|
|
254
|
+
if (value === null || !Number.isFinite(value) || value <= 0)
|
|
255
|
+
return null;
|
|
256
|
+
return Math.max(1_000, Math.min(SEQUENCE_FAILURE_RETRY_AFTER_MAX_MS, Math.ceil(value)));
|
|
257
|
+
}
|
|
258
|
+
function normalizeHttpStatus(value) {
|
|
259
|
+
if (value === null || !Number.isInteger(value) || value < 100 || value > 599)
|
|
260
|
+
return null;
|
|
261
|
+
return value;
|
|
262
|
+
}
|
|
263
|
+
function firstString(records, keys) {
|
|
264
|
+
for (const record of records) {
|
|
265
|
+
for (const key of keys) {
|
|
266
|
+
const value = record[key];
|
|
267
|
+
if (typeof value === "string" && value.trim())
|
|
268
|
+
return value.trim();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
function firstNumber(records, keys) {
|
|
274
|
+
for (const record of records) {
|
|
275
|
+
for (const key of keys) {
|
|
276
|
+
const value = record[key];
|
|
277
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
278
|
+
return value;
|
|
279
|
+
if (typeof value === "string" && value.trim()) {
|
|
280
|
+
const parsed = Number(value);
|
|
281
|
+
if (Number.isFinite(parsed))
|
|
282
|
+
return parsed;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
function firstBoolean(records, keys) {
|
|
289
|
+
for (const record of records) {
|
|
290
|
+
for (const key of keys) {
|
|
291
|
+
if (typeof record[key] === "boolean")
|
|
292
|
+
return record[key];
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
function asRecord(value) {
|
|
298
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
299
|
+
? value
|
|
300
|
+
: null;
|
|
301
|
+
}
|
|
@@ -8,3 +8,4 @@ export declare function markActiveTelemetryError(message: string, attributes?: T
|
|
|
8
8
|
export declare function recordTelemetryCounter(name: string, value?: number, attributes?: TelemetryAttributes): void;
|
|
9
9
|
export declare function recordTelemetryHistogram(name: string, value: number, attributes?: TelemetryAttributes): void;
|
|
10
10
|
export declare function commonTelemetryAttributes(attributes?: TelemetryAttributes): TelemetryAttributes;
|
|
11
|
+
export declare function metricSafeAttributes(attributes?: TelemetryAttributes): TelemetryAttributes;
|
|
@@ -57,13 +57,13 @@ export function recordTelemetryCounter(name, value = 1, attributes) {
|
|
|
57
57
|
if (!Number.isFinite(value))
|
|
58
58
|
return;
|
|
59
59
|
const counter = getCounter(name);
|
|
60
|
-
counter.add(value, normalizeTelemetryAttributes(commonTelemetryAttributes(attributes)));
|
|
60
|
+
counter.add(value, normalizeTelemetryAttributes(metricSafeAttributes(commonTelemetryAttributes(attributes))));
|
|
61
61
|
}
|
|
62
62
|
export function recordTelemetryHistogram(name, value, attributes) {
|
|
63
63
|
if (!Number.isFinite(value))
|
|
64
64
|
return;
|
|
65
65
|
const histogram = getHistogram(name);
|
|
66
|
-
histogram.record(value, normalizeTelemetryAttributes(commonTelemetryAttributes(attributes)));
|
|
66
|
+
histogram.record(value, normalizeTelemetryAttributes(metricSafeAttributes(commonTelemetryAttributes(attributes))));
|
|
67
67
|
}
|
|
68
68
|
export function commonTelemetryAttributes(attributes) {
|
|
69
69
|
const workerProcess = isWorkerProcess();
|
|
@@ -85,6 +85,123 @@ export function commonTelemetryAttributes(attributes) {
|
|
|
85
85
|
...attributes,
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
|
+
// Metrics are billed by *series count*, not by call volume. The OTLP exporter
|
|
89
|
+
// defaults to CUMULATIVE temporality, which means every series a process has
|
|
90
|
+
// ever recorded is re-sent — uncompressed, in full — on every 60s export tick
|
|
91
|
+
// for the entire lifetime of the process. Series never retire. So one attribute
|
|
92
|
+
// whose value varies per request (`trace.id` above all) does not add a label:
|
|
93
|
+
// it turns a single metric into an unbounded, permanently growing set of series,
|
|
94
|
+
// and a histogram multiplies that by ~20 bucket datapoints per series.
|
|
95
|
+
//
|
|
96
|
+
// This is not theoretical. Measured on the Fly worker, outbound traffic GREW
|
|
97
|
+
// with process uptime — 1.70 MB/s at 6.5min, 2.36 MB/s at 8min, 5.56 MB/s
|
|
98
|
+
// averaged over 2.5h — against a flat ~0.4-1.0 MB/s inbound, which is exactly
|
|
99
|
+
// the shape of a re-export set that only ever gets bigger. Fly bills egress at
|
|
100
|
+
// $0.02/GB with no free allowance, putting this one leak at ~$125-290/month.
|
|
101
|
+
// Corroboration: the oxygen-metrics-dev Axiom dataset held 860,493,157
|
|
102
|
+
// datapoints / 935.79 GB from ONE otherwise-idle dev worker, against 36.95 GB
|
|
103
|
+
// for all traces and 12.21 GB for all logs combined.
|
|
104
|
+
//
|
|
105
|
+
// Hence an ALLOW-LIST rather than a deny-list: a future call site that hands a
|
|
106
|
+
// recorder a fresh id-shaped attribute must be structurally unable to
|
|
107
|
+
// reintroduce unbounded series without someone editing this set on purpose.
|
|
108
|
+
// Every key below has a small, closed value domain. Ids and free-form paths
|
|
109
|
+
// (`trace.id`, `run.id`, `org.id`, `tool.id`, `url.path`, `retry.after_ms`)
|
|
110
|
+
// are dropped here and here only — they stay on SPANS, which carry the full
|
|
111
|
+
// attribute set, are written once, and are never re-exported. Do not add an id
|
|
112
|
+
// to this list; if you need to slice a metric by one, you want a span or a log.
|
|
113
|
+
//
|
|
114
|
+
// The same rule kills a subtler case: several worker call sites pass a
|
|
115
|
+
// *measurement* as an attribute (table_action.claimed, queue.probed_tenant_count,
|
|
116
|
+
// workflow.completed, table_ingestion.item_count, worker.tenant_count, ...).
|
|
117
|
+
// An arbitrary integer is not a dimension — each distinct count minted its own
|
|
118
|
+
// permanent series. Those are dropped here too; a count belongs in the metric's
|
|
119
|
+
// VALUE, not in its labels.
|
|
120
|
+
//
|
|
121
|
+
// One exclusion is worth naming because it reads bounded and is not: `route`
|
|
122
|
+
// from apps/web/src/lib/cli-http.ts is `new URL(request.url).pathname` (:1067),
|
|
123
|
+
// the RAW path — so /api/cli/tables/<tableId>/rows carries a table id and mints
|
|
124
|
+
// a series per table. `command` covers the same question without the ids.
|
|
125
|
+
const METRIC_SAFE_ATTRIBUTE_KEYS = new Set([
|
|
126
|
+
// Bounded dimensions supplied by call sites. This list was derived by walking
|
|
127
|
+
// all 123 recordTelemetry* call sites, not from one call site's vocabulary:
|
|
128
|
+
// the same concept is spelled differently in different surfaces (`method` in
|
|
129
|
+
// apps/web/src/lib/cli-http.ts vs `http.request.method` in provider-fetch,
|
|
130
|
+
// `role` in supervisor.ts vs `worker.role` in worker-step.ts), and an
|
|
131
|
+
// allow-list that knows only one spelling silently strips the other.
|
|
132
|
+
"surface",
|
|
133
|
+
"outcome",
|
|
134
|
+
"status",
|
|
135
|
+
"provider.name",
|
|
136
|
+
"provider.operation",
|
|
137
|
+
"credential.mode",
|
|
138
|
+
"http.request.method",
|
|
139
|
+
"http.response.status_code",
|
|
140
|
+
"error.code",
|
|
141
|
+
"retry.attempt",
|
|
142
|
+
"rate_limit.source",
|
|
143
|
+
// apps/web/src/lib/cli-http.ts builds oxygen.cli.commands /
|
|
144
|
+
// oxygen.cli.command.duration_ms from snake_case keys. `command` is the whole
|
|
145
|
+
// point of that counter — "which command is failing" is the only question it
|
|
146
|
+
// exists to answer — so these must survive.
|
|
147
|
+
"command",
|
|
148
|
+
"method",
|
|
149
|
+
"ok",
|
|
150
|
+
"error_code",
|
|
151
|
+
"auth_type",
|
|
152
|
+
// Bounded worker/lane dimensions. These are the natural breakdowns for the
|
|
153
|
+
// oxygen.worker.* instruments (a step name, a lifecycle phase, a queue lane),
|
|
154
|
+
// and every one of them is a closed enum defined in this repo, not a value
|
|
155
|
+
// derived from customer data. They are what makes a worker metric readable at
|
|
156
|
+
// all, so they are kept deliberately — dropping them would have traded an
|
|
157
|
+
// unbounded series count for a useless one.
|
|
158
|
+
//
|
|
159
|
+
// Note what is NOT here: `worker.id`. Per-Machine slicing already survives via
|
|
160
|
+
// the OTel *resource* attributes (resource.host.name / resource.process.pid),
|
|
161
|
+
// which are attached once per export rather than multiplied into every series,
|
|
162
|
+
// so adding it here would multiply the whole metric set by the fleet size for
|
|
163
|
+
// a breakdown that is already available.
|
|
164
|
+
"worker.role",
|
|
165
|
+
"role",
|
|
166
|
+
"worker.phase",
|
|
167
|
+
"worker.step",
|
|
168
|
+
"worker.step.effect",
|
|
169
|
+
"worker.advisory_loop",
|
|
170
|
+
"worker.once",
|
|
171
|
+
"worker.cycle.kind",
|
|
172
|
+
"worker.cycle.phase",
|
|
173
|
+
"worker.cycle.slow",
|
|
174
|
+
"table_action.lane",
|
|
175
|
+
"tenant.ticket_reason",
|
|
176
|
+
"queue.stalled",
|
|
177
|
+
"scope",
|
|
178
|
+
"rail",
|
|
179
|
+
"overall_status",
|
|
180
|
+
"provider",
|
|
181
|
+
"provider_status",
|
|
182
|
+
// Bounded per-process dimensions injected by commonTelemetryAttributes. These
|
|
183
|
+
// are constant for the lifetime of a process, so they cost one series each.
|
|
184
|
+
"oxygen.version",
|
|
185
|
+
"deployment.environment",
|
|
186
|
+
"deployment.sha",
|
|
187
|
+
"deployment.git_sha",
|
|
188
|
+
"cloud.region",
|
|
189
|
+
]);
|
|
190
|
+
// Narrows an attribute bag to METRIC_SAFE_ATTRIBUTE_KEYS. Applied only by
|
|
191
|
+
// recordTelemetryCounter / recordTelemetryHistogram — withTelemetrySpan,
|
|
192
|
+
// setActiveTelemetryAttributes and markActiveTelemetryError deliberately keep
|
|
193
|
+
// every attribute they are given, because spans are what engineers debug with
|
|
194
|
+
// and cost one write rather than one series re-sent every minute forever.
|
|
195
|
+
export function metricSafeAttributes(attributes) {
|
|
196
|
+
if (!attributes)
|
|
197
|
+
return {};
|
|
198
|
+
const output = {};
|
|
199
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
200
|
+
if (METRIC_SAFE_ATTRIBUTE_KEYS.has(key))
|
|
201
|
+
output[key] = value;
|
|
202
|
+
}
|
|
203
|
+
return output;
|
|
204
|
+
}
|
|
88
205
|
// Picks the env-var fallback list for the active surface (worker vs web), then
|
|
89
206
|
// returns the first env var in that list that is set, else null. Preserves the
|
|
90
207
|
// original `a ?? b ?? null` semantics exactly: only an unset (`undefined`) env
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const OXYGEN_VERSION = "1.
|
|
1
|
+
export declare const OXYGEN_VERSION = "1.879.0";
|
|
2
2
|
export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.181.0";
|
|
3
3
|
export declare const MANAGED_INBOX_MINIMUM_CLI_VERSION = "1.326.2";
|
|
4
4
|
export declare const SUPPORT_AGENT_REPLY_MINIMUM_CLI_VERSION = "1.747.0";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const OXYGEN_VERSION = "1.
|
|
1
|
+
export const OXYGEN_VERSION = "1.879.0";
|
|
2
2
|
// The GLOBAL CLI compatibility floor: the oldest CLI allowed to call any
|
|
3
3
|
// operational route. Raising it hard-rejects every older CLI from the entire
|
|
4
4
|
// product, so it obeys one law, enforced by scripts/ci/cli-min-version-gate.mjs:
|
|
@@ -56,6 +56,21 @@
|
|
|
56
56
|
"import": "./dist/column-types.js",
|
|
57
57
|
"default": "./dist/column-types.js"
|
|
58
58
|
},
|
|
59
|
+
"./json-path": {
|
|
60
|
+
"types": "./dist/json-path.d.ts",
|
|
61
|
+
"import": "./dist/json-path.js",
|
|
62
|
+
"default": "./dist/json-path.js"
|
|
63
|
+
},
|
|
64
|
+
"./column-output-fields": {
|
|
65
|
+
"types": "./dist/column-output-fields.d.ts",
|
|
66
|
+
"import": "./dist/column-output-fields.js",
|
|
67
|
+
"default": "./dist/column-output-fields.js"
|
|
68
|
+
},
|
|
69
|
+
"./research-output-contract": {
|
|
70
|
+
"types": "./dist/research-output-contract.d.ts",
|
|
71
|
+
"import": "./dist/research-output-contract.js",
|
|
72
|
+
"default": "./dist/research-output-contract.js"
|
|
73
|
+
},
|
|
59
74
|
"./object-storage": {
|
|
60
75
|
"types": "./dist/object-storage.d.ts",
|
|
61
76
|
"import": "./dist/object-storage.js",
|