@arizeai/phoenix-client
Version:
A client for the Phoenix API
238 lines • 9.41 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SpanCreationError = void 0;
exports.logSpans = logSpans;
const client_1 = require("../client");
const errors_1 = require("../errors");
const projects_1 = require("../types/projects");
const apiErrorUtils_1 = require("../utils/apiErrorUtils");
const isObject_1 = require("../utils/isObject");
const safelyParseJSON_1 = require("../utils/safelyParseJSON");
/**
* Raised by {@link logSpans} when one or more spans in the request are invalid
* or duplicates. If any span in a request fails, none of the spans in that
* request are queued.
*/
class SpanCreationError extends Error {
constructor(params) {
var _a, _b, _c, _d;
super(params.message);
this.name = "SpanCreationError";
this.invalidSpans = (_a = params.invalidSpans) !== null && _a !== void 0 ? _a : [];
this.duplicateSpans = (_b = params.duplicateSpans) !== null && _b !== void 0 ? _b : [];
this.totalReceived = (_c = params.totalReceived) !== null && _c !== void 0 ? _c : 0;
this.totalQueued = (_d = params.totalQueued) !== null && _d !== void 0 ? _d : 0;
}
/** Number of spans rejected as invalid. */
get totalInvalid() {
return this.invalidSpans.length;
}
/** Number of spans rejected as duplicates. */
get totalDuplicates() {
return this.duplicateSpans.length;
}
}
exports.SpanCreationError = SpanCreationError;
const MAX_ERRORS_TO_SHOW = 5;
function formatLogSpansErrorMessage({ invalidSpans, duplicateSpans, }) {
const parts = [];
if (invalidSpans.length > 0) {
parts.push(`Failed to queue ${invalidSpans.length} invalid spans:`);
for (const span of invalidSpans.slice(0, MAX_ERRORS_TO_SHOW)) {
parts.push(` - Span ${span.spanId}: ${span.error}`);
}
if (invalidSpans.length > MAX_ERRORS_TO_SHOW) {
parts.push(` ... and ${invalidSpans.length - MAX_ERRORS_TO_SHOW} more invalid spans`);
}
}
if (duplicateSpans.length > 0) {
if (parts.length > 0)
parts.push("");
parts.push(`Found ${duplicateSpans.length} duplicate spans:`);
for (const span of duplicateSpans.slice(0, MAX_ERRORS_TO_SHOW)) {
parts.push(` - Span ${span.spanId}`);
}
if (duplicateSpans.length > MAX_ERRORS_TO_SHOW) {
parts.push(` ... and ${duplicateSpans.length - MAX_ERRORS_TO_SHOW} more duplicates`);
}
}
return parts.join("\n");
}
function toSpanIdAndTraceId(item) {
const record = (0, isObject_1.isObject)(item) ? item : {};
return {
spanId: typeof record.span_id === "string" ? record.span_id : "unknown",
traceId: typeof record.trace_id === "string" ? record.trace_id : "unknown",
};
}
function toInvalidSpanInfoList(value) {
if (!Array.isArray(value))
return [];
return value.map((item) => {
const record = (0, isObject_1.isObject)(item) ? item : {};
return Object.assign(Object.assign({}, toSpanIdAndTraceId(item)), { error: typeof record.error === "string" ? record.error : "Validation error" });
});
}
function toDuplicateSpanInfoList(value) {
if (!Array.isArray(value))
return [];
return value.map(toSpanIdAndTraceId);
}
/**
* Builds a {@link SpanCreationError} from already-normalized invalid/duplicate
* span lists, computing the shared message once.
*/
function makeSpanCreationError(params) {
return new SpanCreationError(Object.assign({ message: formatLogSpansErrorMessage(params) }, params));
}
/**
* Builds a {@link SpanCreationError} from a parsed error payload matching the
* shape returned by the server for invalid/duplicate spans:
* `{ total_received, total_queued, total_invalid, total_duplicates, invalid_spans, duplicate_spans }`.
*/
function buildSpanCreationError(payload) {
return makeSpanCreationError({
invalidSpans: toInvalidSpanInfoList(payload.invalid_spans),
duplicateSpans: toDuplicateSpanInfoList(payload.duplicate_spans),
totalReceived: typeof payload.total_received === "number" ? payload.total_received : 0,
totalQueued: typeof payload.total_queued === "number" ? payload.total_queued : 0,
});
}
function isSpanCreationErrorPayload(value) {
return ((0, isObject_1.isObject)(value) && ("invalid_spans" in value || "duplicate_spans" in value));
}
/**
* Extracts invalid span info from a FastAPI request-validation error array
* (returned with a 422 status when a span in the payload is malformed).
*/
function extractInvalidSpansFromValidationErrors(errors, spans) {
var _a, _b;
var _c, _d;
const invalidSpans = [];
for (const rawError of errors) {
if (!(0, isObject_1.isObject)(rawError))
continue;
const loc = rawError.loc;
if (!Array.isArray(loc) ||
loc.length < 3 ||
loc[0] !== "body" ||
loc[1] !== "data" ||
typeof loc[2] !== "number") {
continue;
}
const span = spans[loc[2]];
if (!span)
continue;
const msg = rawError.msg;
invalidSpans.push({
spanId: (_c = (_a = span.context) === null || _a === void 0 ? void 0 : _a.span_id) !== null && _c !== void 0 ? _c : "unknown",
traceId: (_d = (_b = span.context) === null || _b === void 0 ? void 0 : _b.trace_id) !== null && _d !== void 0 ? _d : "unknown",
error: typeof msg === "string" ? msg : "Validation error",
});
}
return invalidSpans;
}
function parseLogSpansError(error, spans) {
if ((0, isObject_1.isObject)(error)) {
const detail = error.detail;
if (typeof detail === "string") {
const { json: parsed } = (0, safelyParseJSON_1.safelyParseJSON)(detail);
if (isSpanCreationErrorPayload(parsed)) {
return buildSpanCreationError(parsed);
}
}
else if (Array.isArray(detail)) {
const invalidSpans = extractInvalidSpansFromValidationErrors(detail, spans);
if (invalidSpans.length > 0) {
return makeSpanCreationError({
invalidSpans,
duplicateSpans: [],
totalReceived: spans.length,
// A FastAPI 422 rejects the entire request body, so no spans are
// queued — matching the all-or-nothing contract and the 400 path.
totalQueued: 0,
});
}
}
else if (isSpanCreationErrorPayload(error)) {
return buildSpanCreationError(error);
}
}
return new Error(`Failed to log spans: ${(0, apiErrorUtils_1.formatApiError)(error)}`);
}
async function parseLogSpansHttpError(error, spans) {
try {
const contentType = error.response.headers.get("content-type");
const payload = (contentType === null || contentType === void 0 ? void 0 : contentType.includes("application/json"))
? await error.response.json()
: await error.response.text();
return parseLogSpansError(payload, spans);
}
catch (_a) {
return parseLogSpansError(error, spans);
}
}
/**
* Log spans to a project using Phoenix's simplified span structure.
*
* If any span in the request is invalid or a duplicate of a span that already
* exists, none of the spans in the request are queued and a
* {@link SpanCreationError} is thrown with details about the failures.
*
* @experimental this function is experimental and may change in the future
*
* @param params - The parameters to log spans
* @returns Statistics about the operation. When successful, `totalQueued`
* equals `totalReceived`.
*
* @example
* ```ts
* const result = await logSpans({
* project: { projectName: "my-project" },
* spans: [
* {
* name: "test",
* context: { trace_id: "123", span_id: "456" },
* span_kind: "CHAIN",
* start_time: "2024-01-01T00:00:00Z",
* end_time: "2024-01-01T00:00:01Z",
* status_code: "OK",
* },
* ],
* });
* console.log(`Queued ${result.totalQueued} spans`);
* ```
*/
async function logSpans({ client: _client, project, spans, }) {
var _a, _b;
const client = _client !== null && _client !== void 0 ? _client : (0, client_1.createClient)();
const projectIdentifier = (0, projects_1.resolveProjectIdentifier)(project);
let response;
try {
response = await client.POST("/v1/projects/{project_identifier}/spans", {
params: {
path: {
project_identifier: projectIdentifier,
},
},
body: {
data: spans,
},
});
}
catch (error) {
if (error instanceof errors_1.HttpError) {
throw await parseLogSpansHttpError(error, spans);
}
throw error;
}
const { data, error } = response;
if (error) {
throw parseLogSpansError(error, spans);
}
return {
totalReceived: (_a = data === null || data === void 0 ? void 0 : data.total_received) !== null && _a !== void 0 ? _a : spans.length,
totalQueued: (_b = data === null || data === void 0 ? void 0 : data.total_queued) !== null && _b !== void 0 ? _b : 0,
};
}
//# sourceMappingURL=logSpans.js.map