diff --git a/examples/04-langchain.ts b/examples/04-langchain.ts index 230918d..e62969e 100644 --- a/examples/04-langchain.ts +++ b/examples/04-langchain.ts @@ -2,6 +2,7 @@ import { ZeroEvalCallbackHandler, setGlobalCallbackHandler, } from "zeroeval/langchain"; +import { init } from "zeroeval" import { ChatOpenAI } from "@langchain/openai"; import { ChatPromptTemplate } from "@langchain/core/prompts"; @@ -106,7 +107,7 @@ async function main() { const structuredReport = await structuredModel.invoke( `Based on this weather information: "${weatherInfo}", generate a detailed weather report.` ); - + console.log("\nStructured Weather Report:"); console.log(JSON.stringify(structuredReport, null, 2)); @@ -132,17 +133,17 @@ async function main() { const structuredReport2 = await structuredModel.invoke( `Based on this weather information: "${weatherInfo2}", generate a detailed weather report.` ); - + console.log("\nStructured Weather Report for NY:"); console.log(JSON.stringify(structuredReport2, null, 2)); // Example 3: Direct structured output without tool calling console.log("\n\nDirect structured output example...\n"); - + const directStructuredResult = await structuredModel.invoke( "Generate a weather report for London, UK. Make it rainy and cold, around 45°F." ); - + console.log("Direct Structured Output:"); console.log(JSON.stringify(directStructuredResult, null, 2)); diff --git a/package-lock.json b/package-lock.json index 26799b5..66fbdad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "zeroeval", - "version": "0.1.6", + "version": "0.1.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "zeroeval", - "version": "0.1.6", + "version": "0.1.7", "license": "Apache-2.0", "devDependencies": { "@eslint/js": "^9.30.1", - "@types/node": "^20.19.4", + "@types/node": "^20.19.6", "@typescript-eslint/eslint-plugin": "^8.36.0", "@typescript-eslint/parser": "^8.36.0", "@vitest/coverage-v8": "^3.2.4", @@ -18,6 +18,7 @@ "eslint": "^9.30.1", "eslint-config-prettier": "^10.1.5", "eslint-plugin-prettier": "^5.5.1", + "globals": "^16.3.0", "prettier": "^3.6.2", "rimraf": "^5.0.0", "ts-node": "^10.9.2", @@ -827,6 +828,19 @@ "concat-map": "0.0.1" } }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1655,9 +1669,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.4.tgz", - "integrity": "sha512-OP+We5WV8Xnbuvw0zC2m4qfB/BJvjyCwtNjhHdJxV1639SGSKrLmJkc3fMnp2Qy8nJyHp8RO6umxELN/dS1/EA==", + "version": "20.19.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.6.tgz", + "integrity": "sha512-uYssdp9z5zH5GQ0L4zEJ2ZuavYsJwkozjiUzCRfGtaaQcyjAMJ34aP8idv61QlqTozu6kudyr6JMq9Chf09dfA==", "dev": true, "license": "MIT", "dependencies": { @@ -3231,9 +3245,9 @@ } }, "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 4de6a22..b2331b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zeroeval", - "version": "0.1.6", + "version": "0.1.8", "description": "ZeroEval TypeScript SDK", "keywords": [ "observability", @@ -112,7 +112,7 @@ }, "devDependencies": { "@eslint/js": "^9.30.1", - "@types/node": "^20.19.4", + "@types/node": "^20.19.6", "@typescript-eslint/eslint-plugin": "^8.36.0", "@typescript-eslint/parser": "^8.36.0", "@vitest/coverage-v8": "^3.2.4", @@ -120,6 +120,7 @@ "eslint": "^9.30.1", "eslint-config-prettier": "^10.1.5", "eslint-plugin-prettier": "^5.5.1", + "globals": "^16.3.0", "prettier": "^3.6.2", "rimraf": "^5.0.0", "ts-node": "^10.9.2", diff --git a/src/helpers.ts b/src/helpers.ts index 62a1516..5c42499 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,5 +1,5 @@ import { tracer } from './observability/Tracer'; -import { Span } from './observability/Span'; +import type { Span } from './observability/Span'; /** Return the current active Span (or undefined). */ export function getCurrentSpan(): Span | undefined { diff --git a/src/init.ts b/src/init.ts index 7cd1154..403f12d 100644 --- a/src/init.ts +++ b/src/init.ts @@ -1,4 +1,7 @@ import { tracer } from './observability/Tracer'; +import { Logger, getLogger } from './observability/logger'; + +const logger = getLogger('zeroeval'); export interface InitOptions { apiKey?: string; @@ -7,6 +10,7 @@ export interface InitOptions { maxSpans?: number; collectCodeDetails?: boolean; integrations?: Record; + debug?: boolean; } // Track whether init has been called @@ -32,8 +36,40 @@ export function init(opts: InitOptions = {}): void { maxSpans, collectCodeDetails, integrations, + debug, } = opts; + // Check if debug mode is enabled via param or env var + const isDebugMode = + debug || process.env.ZEROEVAL_DEBUG?.toLowerCase() === 'true'; + + // Enable debug mode + if (isDebugMode) { + process.env.ZEROEVAL_DEBUG = 'true'; + Logger.setDebugMode(true); + + // Log all configuration values as the first log message + const maskedApiKey = Logger.maskApiKey( + apiKey || process.env.ZEROEVAL_API_KEY + ); + const finalApiUrl = + apiUrl || process.env.ZEROEVAL_API_URL || 'https://api.zeroeval.com'; + + logger.debug('ZeroEval SDK Configuration:'); + logger.debug(` API Key: ${maskedApiKey}`); + logger.debug(` API URL: ${finalApiUrl}`); + logger.debug(` Debug Mode: ${isDebugMode}`); + logger.debug(` Flush Interval: ${flushInterval ?? '10s (default)'}`); + logger.debug(` Max Spans: ${maxSpans ?? '100 (default)'}`); + logger.debug( + ` Collect Code Details: ${collectCodeDetails ?? 'true (default)'}` + ); + + logger.info('SDK initialized in debug mode.'); + } else { + Logger.setDebugMode(false); + } + if (apiKey) process.env.ZEROEVAL_API_KEY = apiKey; if (apiUrl) process.env.ZEROEVAL_API_URL = apiUrl; diff --git a/src/observability/Tracer.ts b/src/observability/Tracer.ts index 45d6814..3b38668 100644 --- a/src/observability/Tracer.ts +++ b/src/observability/Tracer.ts @@ -1,10 +1,14 @@ import { AsyncLocalStorage } from 'async_hooks'; import { randomUUID } from 'crypto'; import { Span } from './Span'; -import { SpanWriter, BackendSpanWriter } from './writer'; +import type { SpanWriter } from './writer'; +import { BackendSpanWriter } from './writer'; import { setInterval } from 'timers'; import { discoverIntegrations } from './integrations/utils'; import type { Integration } from './integrations/base'; +import { getLogger } from './logger'; + +const logger = getLogger('zeroeval.tracer'); interface ConfigureOptions { flushInterval?: number; @@ -30,6 +34,11 @@ export class Tracer { private _shuttingDown = false; constructor() { + logger.debug('Initializing tracer...'); + logger.debug( + `Tracer config: flush_interval=${this._flushIntervalMs}ms, max_spans=${this._maxSpans}` + ); + // schedule periodic flush setInterval(() => { if (Date.now() - this._lastFlush >= this._flushIntervalMs) { @@ -54,10 +63,17 @@ export class Tracer { /* CONFIG ----------------------------------------------------------------*/ configure(opts: ConfigureOptions = {}) { - if (opts.flushInterval !== undefined) + if (opts.flushInterval !== undefined) { this._flushIntervalMs = opts.flushInterval * 1000; - if (opts.maxSpans !== undefined) this._maxSpans = opts.maxSpans; - // Other options ignored for now (collectCodeDetails, integrations) + logger.info( + `Tracer flush_interval configured to ${opts.flushInterval}s.` + ); + } + if (opts.maxSpans !== undefined) { + this._maxSpans = opts.maxSpans; + logger.info(`Tracer max_spans configured to ${opts.maxSpans}.`); + } + logger.debug(`Tracer configuration updated:`, opts); } /* ACTIVE SPAN -----------------------------------------------------------*/ @@ -76,6 +92,8 @@ export class Tracer { tags?: Record; } = {} ): Span { + logger.debug(`Starting span: ${name}`); + const parent = this.currentSpan(); const span = new Span(name, parent?.traceId); @@ -85,10 +103,14 @@ export class Tracer { span.sessionName = parent.sessionName; // inherit tags span.tags = { ...parent.tags, ...opts.tags }; + logger.debug(`Span ${name} inherits from parent ${parent.name}`); } else { span.sessionId = opts.sessionId ?? randomUUID(); span.sessionName = opts.sessionName; span.tags = { ...opts.tags }; + logger.debug( + `Span ${name} is a root span with session ${span.sessionId}` + ); } Object.assign(span.attributes, opts.attributes); @@ -107,6 +129,8 @@ export class Tracer { endSpan(span: Span): void { if (!span.endTime) span.end(); + logger.debug(`Ending span: ${span.name} (duration: ${span.durationMs}ms)`); + // pop stack const stack = als.getStore(); if (stack && stack[stack.length - 1] === span) { @@ -124,14 +148,25 @@ export class Tracer { const ordered = traceBucket.sort((a) => (a.parentId ? 1 : -1)); delete this._traceBuckets[span.traceId]; this._buffer.push(...ordered); + + logger.debug( + `Trace ${span.traceId} complete with ${ordered.length} spans` + ); } // flush if buffer full - if (this._buffer.length >= this._maxSpans) this.flush(); + if (this._buffer.length >= this._maxSpans) { + logger.debug( + `Buffer full (${this._buffer.length} spans), triggering flush` + ); + this.flush(); + } } /* TAG HELPERS -----------------------------------------------------------*/ addTraceTags(traceId: string, tags: Record): void { + logger.debug(`Adding trace tags to ${traceId}:`, tags); + // update buckets for (const span of this._traceBuckets[traceId] ?? []) Object.assign(span.tags, tags); @@ -142,6 +177,8 @@ export class Tracer { } addSessionTags(sessionId: string, tags: Record): void { + logger.debug(`Adding session tags to ${sessionId}:`, tags); + const all = [...Object.values(this._traceBuckets).flat(), ...this._buffer]; all .filter((s) => s.sessionId === sessionId) @@ -155,24 +192,39 @@ export class Tracer { /* FLUSH -----------------------------------------------------------------*/ flush(): void { if (this._buffer.length === 0) return; + + logger.info(`Flushing ${this._buffer.length} spans to backend`); + this._lastFlush = Date.now(); this._writer.write(this._buffer.splice(0)); } private async _setupAvailableIntegrations(): Promise { + logger.info('Checking for available integrations...'); + const available = await discoverIntegrations(); + for (const [key, Ctor] of Object.entries(available)) { try { const inst = new Ctor(); if ((Ctor as any).isAvailable?.() !== false) { + logger.info(`Setting up integration: ${key}`); inst.setup(); this._integrations[key] = inst; + logger.info(`✅ Successfully set up integration: ${key}`); } } catch (err) { - // eslint-disable-next-line no-console - console.warn(`[ZeroEval] Failed to setup integration ${key}`, err); + logger.error(`❌ Failed to setup integration ${key}:`, err); } } + + if (Object.keys(this._integrations).length > 0) { + logger.info( + `Active integrations: ${Object.keys(this._integrations).join(', ')}` + ); + } else { + logger.info('No active integrations found.'); + } } /** Flush remaining spans and teardown integrations */ @@ -180,6 +232,8 @@ export class Tracer { if (this._shuttingDown) return; this._shuttingDown = true; + logger.info('Shutting down tracer...'); + try { this.flush(); } catch (_) {} diff --git a/src/observability/integrations/langchain.ts b/src/observability/integrations/langchain.ts index 302508d..705ee11 100644 --- a/src/observability/integrations/langchain.ts +++ b/src/observability/integrations/langchain.ts @@ -48,7 +48,7 @@ export class LangChainIntegration extends Integration { for (const method of methods) { if (typeof Runnable.prototype[method] !== 'function') continue; this.patchMethod( - Runnable.prototype as any, + Runnable.prototype, method as any, (orig: AnyFn): AnyFn => { const isAsync = method.toString().startsWith('a'); diff --git a/src/observability/integrations/langchain/ZeroEvalCallbackHandler.ts b/src/observability/integrations/langchain/ZeroEvalCallbackHandler.ts index 1434a3f..eb60c89 100644 --- a/src/observability/integrations/langchain/ZeroEvalCallbackHandler.ts +++ b/src/observability/integrations/langchain/ZeroEvalCallbackHandler.ts @@ -1,22 +1,23 @@ -import { - BaseCallbackHandler, - BaseCallbackHandlerInput, -} from '@langchain/core/callbacks/base'; -import { AgentAction, AgentFinish } from '@langchain/core/dist/agents'; -import { DocumentInterface } from '@langchain/core/dist/documents/document'; -import { Serialized } from '@langchain/core/dist/load/serializable'; -import { BaseMessage } from '@langchain/core/dist/messages/base'; -import { +import type { BaseCallbackHandlerInput } from '@langchain/core/callbacks/base'; +import { BaseCallbackHandler } from '@langchain/core/callbacks/base'; +import type { AgentAction, AgentFinish } from '@langchain/core/dist/agents'; +import type { DocumentInterface } from '@langchain/core/dist/documents/document'; +import type { Serialized } from '@langchain/core/dist/load/serializable'; +import type { BaseMessage } from '@langchain/core/dist/messages/base'; +import type { ChatGeneration, ChatResult, Generation, LLMResult, } from '@langchain/core/dist/outputs'; -import { ChainValues } from '@langchain/core/dist/utils/types'; +import type { ChainValues } from '@langchain/core/dist/utils/types'; import { ToolMessage } from '@langchain/core/messages'; -import { RunnableConfig } from '@langchain/core/runnables'; +import type { RunnableConfig } from '@langchain/core/runnables'; import { tracer } from '../../Tracer'; -import { Span } from '../../Span'; +import type { Span } from '../../Span'; +import { getLogger } from '../../logger'; + +const logger = getLogger('zeroeval.langchain'); export interface ZeroEvalCallbackHandlerOptions { debug?: boolean; @@ -160,14 +161,12 @@ export class ZeroEvalCallbackHandler metadata?: Record; }) { if (this.spans.has(runId)) { - if (this.options.debug) { - console.warn(`Span already exists for runId ${runId}`); - } + logger.warn(`Span already exists for runId ${runId}`); return; } if (this.spans.size >= this.options.maxConcurrentSpans) { - console.warn( + logger.warn( `Max concurrent spans (${this.options.maxConcurrentSpans}) reached` ); return; @@ -240,9 +239,7 @@ export class ZeroEvalCallbackHandler }): void { const span = this.spans.get(runId); if (!span) { - if (this.options.debug) { - console.warn(`No span exists for runId ${runId}`); - } + logger.warn(`No span exists for runId ${runId}`); return; } diff --git a/src/observability/integrations/langchain/setGlobalCallbackHandler.ts b/src/observability/integrations/langchain/setGlobalCallbackHandler.ts index 5ff0fbb..07c30db 100644 --- a/src/observability/integrations/langchain/setGlobalCallbackHandler.ts +++ b/src/observability/integrations/langchain/setGlobalCallbackHandler.ts @@ -1,4 +1,4 @@ -import { BaseCallbackHandler } from '@langchain/core/callbacks/base'; +import type { BaseCallbackHandler } from '@langchain/core/callbacks/base'; let globalHandler: BaseCallbackHandler | undefined; diff --git a/src/observability/integrations/vercelAIWrapper.ts b/src/observability/integrations/vercelAIWrapper.ts index fdcf9a1..38ef75b 100644 --- a/src/observability/integrations/vercelAIWrapper.ts +++ b/src/observability/integrations/vercelAIWrapper.ts @@ -102,7 +102,7 @@ function wrapVercelAIFunction( input = JSON.stringify(options); } - const result = await (fn as T)(...args); + const result = await fn(...args); // Handle different result types if (result && typeof result === 'object') { diff --git a/src/observability/integrations/wrapper.ts b/src/observability/integrations/wrapper.ts index f5fc588..e2743a3 100644 --- a/src/observability/integrations/wrapper.ts +++ b/src/observability/integrations/wrapper.ts @@ -9,24 +9,42 @@ type WrappedClient = T & { }; // Type guards for different clients -function isOpenAIClient(client: any): client is InstanceType { +function isOpenAIClient( + client: unknown +): client is InstanceType { // Check for OpenAI-specific properties + if (typeof client !== 'object' || client === null) { + return false; + } + + const obj = client as Record; return ( - client?.chat?.completions?.create !== undefined && - client?.embeddings?.create !== undefined && - client?.constructor?.name === 'OpenAI' + obj.chat !== undefined && + typeof obj.chat === 'object' && + obj.chat !== null && + 'completions' in obj.chat && + obj.embeddings !== undefined && + typeof obj.embeddings === 'object' && + obj.embeddings !== null && + 'create' in obj.embeddings && + obj.constructor !== undefined && + typeof obj.constructor === 'function' && + obj.constructor.name === 'OpenAI' ); } -function isVercelAIModule(client: any): boolean { +function isVercelAIModule(client: unknown): boolean { // Check for Vercel AI SDK functions + if (typeof client !== 'object' || client === null) { + return false; + } + + const obj = client as Record; return ( - typeof client === 'object' && - client !== null && - (typeof client.generateText === 'function' || - typeof client.streamText === 'function' || - typeof client.generateObject === 'function' || - typeof client.embed === 'function') + typeof obj.generateText === 'function' || + typeof obj.streamText === 'function' || + typeof obj.generateObject === 'function' || + typeof obj.embed === 'function' ); } @@ -94,12 +112,23 @@ export function wrap(client: T): WrappedClient { } // If we reach here, the client type is not supported + let clientType = 'unknown'; + if (typeof client === 'object' && client !== null) { + const obj = client as Record; + if (obj.constructor && typeof obj.constructor === 'function') { + const ctor = obj.constructor as { name?: string }; + clientType = ctor.name || 'unknown'; + } + } else { + clientType = typeof client; + } + throw new Error( `Unsupported client type. ze.wrap() currently supports:\n` + `- OpenAI clients (from 'openai' package)\n` + `- Vercel AI SDK (from 'ai' package)\n` + `\n` + - `Received: ${(client as any)?.constructor?.name || typeof client}\n` + + `Received: ${clientType}\n` + `\n` + `Make sure you're passing a valid client instance, e.g.:\n` + ` const openai = ze.wrap(new OpenAI());\n` + diff --git a/src/observability/logger.ts b/src/observability/logger.ts new file mode 100644 index 0000000..9d4a5f1 --- /dev/null +++ b/src/observability/logger.ts @@ -0,0 +1,103 @@ +/** + * Logger utility for ZeroEval TypeScript SDK + * Provides colored console output when debug mode is enabled + */ + +// ANSI color codes +const colors = { + grey: '\x1b[38;5;244m', + blue: '\x1b[34;1m', + green: '\x1b[32m', + yellow: '\x1b[33m', + red: '\x1b[31m', + boldRed: '\x1b[31;1m', + reset: '\x1b[0m', +}; + +export enum LogLevel { + DEBUG = 0, + INFO = 1, + WARN = 2, + ERROR = 3, +} + +class Logger { + private name: string; + private static globalLevel: LogLevel = LogLevel.WARN; + private static isDebugMode: boolean = false; + + constructor(name: string) { + this.name = name; + } + + static setDebugMode(enabled: boolean): void { + Logger.isDebugMode = enabled; + Logger.globalLevel = enabled ? LogLevel.DEBUG : LogLevel.WARN; + } + + static isDebugEnabled(): boolean { + return Logger.isDebugMode; + } + + private formatTimestamp(): string { + const now = new Date(); + const hours = now.getHours().toString().padStart(2, '0'); + const minutes = now.getMinutes().toString().padStart(2, '0'); + const seconds = now.getSeconds().toString().padStart(2, '0'); + return `${hours}:${minutes}:${seconds}`; + } + + private formatMessage( + level: string, + levelColor: string, + message: string + ): string { + if (!Logger.isDebugMode) { + // In non-debug mode, use simple format without colors + return `[${this.name}] [${level}] ${message}`; + } + + // In debug mode, use colored format + const timestamp = this.formatTimestamp(); + return `${colors.grey}[${timestamp}]${colors.reset} ${colors.blue}[${this.name}]${colors.reset} ${levelColor}[${level}]${colors.reset} ${message}`; + } + + debug(message: string, ...args: unknown[]): void { + if (Logger.globalLevel <= LogLevel.DEBUG) { + console.log(this.formatMessage('DEBUG', colors.blue, message), ...args); + } + } + + info(message: string, ...args: unknown[]): void { + if (Logger.globalLevel <= LogLevel.INFO) { + console.log(this.formatMessage('INFO', colors.green, message), ...args); + } + } + + warn(message: string, ...args: unknown[]): void { + if (Logger.globalLevel <= LogLevel.WARN) { + console.warn(this.formatMessage('WARN', colors.yellow, message), ...args); + } + } + + error(message: string, ...args: unknown[]): void { + if (Logger.globalLevel <= LogLevel.ERROR) { + console.error(this.formatMessage('ERROR', colors.red, message), ...args); + } + } + + /** + * Mask sensitive data like API keys for logging + */ + static maskApiKey(apiKey: string | undefined): string { + if (!apiKey) return 'Not set'; + if (apiKey.length <= 8) return '***'; + return `${apiKey.substring(0, 8)}...`; + } +} + +export function getLogger(name: string): Logger { + return new Logger(name); +} + +export { Logger }; diff --git a/src/observability/signalWriter.ts b/src/observability/signalWriter.ts index 2aed5e8..b828ada 100644 --- a/src/observability/signalWriter.ts +++ b/src/observability/signalWriter.ts @@ -3,6 +3,9 @@ import type { BulkSignalsCreate, SignalResponse, } from './signals'; +import { getLogger } from './logger'; + +const logger = getLogger('zeroeval.signalWriter'); export class SignalWriter { private getApiUrl(): string { @@ -37,7 +40,7 @@ export class SignalWriter { if (!res.ok) { const text = await res.text(); - console.error( + logger.error( `[ZeroEval] Failed creating signal: ${res.status} ${text}` ); return { @@ -46,9 +49,9 @@ export class SignalWriter { }; } - return await res.json(); + return (await res.json()) as SignalResponse; } catch (err) { - console.error('[ZeroEval] Error creating signal', err); + logger.error('[ZeroEval] Error creating signal', err); return { status: 'error', message: `Error creating signal: ${err instanceof Error ? err.message : String(err)}`, @@ -79,7 +82,7 @@ export class SignalWriter { if (!res.ok) { const text = await res.text(); - console.error( + logger.error( `[ZeroEval] Failed creating bulk signals: ${res.status} ${text}` ); return { @@ -88,9 +91,9 @@ export class SignalWriter { }; } - return await res.json(); + return (await res.json()) as SignalResponse; } catch (err) { - console.error('[ZeroEval] Error creating bulk signals', err); + logger.error('[ZeroEval] Error creating bulk signals', err); return { status: 'error', message: `Error creating bulk signals: ${err instanceof Error ? err.message : String(err)}`, @@ -118,7 +121,7 @@ export class SignalWriter { if (!res.ok) { const text = await res.text(); - console.error( + logger.error( `[ZeroEval] Failed getting entity signals: ${res.status} ${text}` ); return null; @@ -126,7 +129,7 @@ export class SignalWriter { return await res.json(); } catch (err) { - console.error('[ZeroEval] Error getting entity signals', err); + logger.error('[ZeroEval] Error getting entity signals', err); return null; } } diff --git a/src/observability/spanDecorator.ts b/src/observability/spanDecorator.ts index 874da80..7510f72 100644 --- a/src/observability/spanDecorator.ts +++ b/src/observability/spanDecorator.ts @@ -1,9 +1,4 @@ import { tracer } from './Tracer'; -import { Span } from './Span'; -import { inspect } from 'util'; -import { randomUUID } from 'crypto'; -import path from 'path'; -import { fileURLToPath } from 'url'; export interface SpanOptions { name: string; diff --git a/src/observability/writer.ts b/src/observability/writer.ts index 5c137aa..3ec691f 100644 --- a/src/observability/writer.ts +++ b/src/observability/writer.ts @@ -1,5 +1,8 @@ import { signalWriter } from './signalWriter'; -import { convertSignalsForBackend, Signal, SignalCreate } from './signals'; +import type { Signal, SignalCreate } from './signals'; +import { getLogger } from './logger'; + +const logger = getLogger('zeroeval.writer'); type PendingFns = { popPendingTraceSignals: (id: string) => Record | undefined; @@ -82,7 +85,7 @@ export class BackendSpanWriter implements SpanWriter { }); if (!res.ok) { const text = await res.text(); - console.error(`[ZeroEval] Failed posting spans: ${res.status} ${text}`); + logger.error(`[ZeroEval] Failed posting spans: ${res.status} ${text}`); } else { // Send span-level signals await this.sendSpanSignals(spansWithSignals); @@ -93,7 +96,7 @@ export class BackendSpanWriter implements SpanWriter { ); } } catch (err) { - console.error('[ZeroEval] Error posting spans', err); + logger.error('[ZeroEval] Error posting spans', err); } } @@ -121,7 +124,7 @@ export class BackendSpanWriter implements SpanWriter { try { await signalWriter.createBulkSignals(bulkSignals); } catch (err) { - console.error('[ZeroEval] Error sending span signals', err); + logger.error('[ZeroEval] Error sending span signals', err); } } } @@ -170,7 +173,7 @@ export class BackendSpanWriter implements SpanWriter { try { await signalWriter.createBulkSignals(bulk); } catch (err) { - console.error('[ZeroEval] Error posting trace/session signals', err); + logger.error('[ZeroEval] Error posting trace/session signals', err); } } } diff --git a/src/signals.ts b/src/signals.ts index 46b9889..3dacfcf 100644 --- a/src/signals.ts +++ b/src/signals.ts @@ -1,14 +1,14 @@ import { signalWriter } from './observability/signalWriter'; -import { - Signal, - SignalCreate, - detectSignalType, -} from './observability/signals'; +import type { Signal, SignalCreate } from './observability/signals'; +import { detectSignalType } from './observability/signals'; import { tracer } from './observability/Tracer'; import { addPendingTraceSignal, addPendingSessionSignal, } from './observability/pendingSignals'; +import { getLogger } from './observability/logger'; + +const logger = getLogger('zeroeval.signals'); /** * Send a signal to a specific entity @@ -50,14 +50,14 @@ export async function sendBulkSignals(signals: SignalCreate[]): Promise { * @param value - Signal value (string, boolean, or number) * @param signalType - Optional signal type, will be auto-detected if not provided */ -export async function sendTraceSignal( +export function sendTraceSignal( name: string, value: string | boolean | number, signalType?: 'boolean' | 'numerical' -): Promise { +): void { const currentSpan = tracer.currentSpan(); if (!currentSpan) { - console.warn( + logger.warn( '[ZeroEval] No active span/trace found for sending trace signal' ); return; @@ -75,14 +75,14 @@ export async function sendTraceSignal( * @param value - Signal value (string, boolean, or number) * @param signalType - Optional signal type, will be auto-detected if not provided */ -export async function sendSessionSignal( +export function sendSessionSignal( name: string, value: string | boolean | number, signalType?: 'boolean' | 'numerical' -): Promise { +): void { const currentSpan = tracer.currentSpan(); if (!currentSpan || !currentSpan.sessionId) { - console.warn( + logger.warn( '[ZeroEval] No active session found for sending session signal' ); return; @@ -100,14 +100,14 @@ export async function sendSessionSignal( * @param value - Signal value (string, boolean, or number) * @param signalType - Optional signal type, will be auto-detected if not provided */ -export async function sendSpanSignal( +export function sendSpanSignal( name: string, value: string | boolean | number, signalType?: 'boolean' | 'numerical' -): Promise { +): void { const currentSpan = tracer.currentSpan(); if (!currentSpan) { - console.warn('[ZeroEval] No active span found for sending span signal'); + logger.warn('[ZeroEval] No active span found for sending span signal'); return; } @@ -125,5 +125,13 @@ export async function getEntitySignals( entityType: 'session' | 'trace' | 'span' | 'completion', entityId: string ): Promise { - return await signalWriter.getEntitySignals(entityType, entityId); + const result = (await signalWriter.getEntitySignals( + entityType, + entityId + )) as unknown; + // Handle null or invalid responses + if (!result || !Array.isArray(result)) { + return []; + } + return result as Signal[]; } diff --git a/tsconfig.json b/tsconfig.json index de3c8a5..1914348 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "target": "ES2020", "module": "ESNext", "moduleResolution": "Node", + "lib": ["ES2020", "DOM"], "declaration": true, "declarationDir": "./dist/types", "outDir": "./dist/esm", @@ -20,4 +21,4 @@ }, "include": ["src"], "exclude": ["node_modules", "dist"] -} \ No newline at end of file +}