UNPKG

@arizeai/phoenix-client

Version:
238 lines 9.41 kB
"use strict"; 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