langwatch 0.1.7 → 0.3.0-prerelease.1
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/.editorconfig +16 -0
- package/LICENSE +7 -0
- package/README.md +268 -1
- package/copy-types.sh +19 -8
- package/examples/langchain/.env.example +2 -0
- package/examples/langchain/README.md +42 -0
- package/examples/langchain/package-lock.json +2930 -0
- package/examples/langchain/package.json +27 -0
- package/examples/langchain/src/cli-markdown.d.ts +137 -0
- package/examples/langchain/src/index.ts +109 -0
- package/examples/langchain/tsconfig.json +25 -0
- package/examples/langgraph/.env.example +2 -0
- package/examples/langgraph/README.md +42 -0
- package/examples/langgraph/package-lock.json +3031 -0
- package/examples/langgraph/package.json +28 -0
- package/examples/langgraph/src/cli-markdown.d.ts +137 -0
- package/examples/langgraph/src/index.ts +196 -0
- package/examples/langgraph/tsconfig.json +25 -0
- package/examples/mastra/.env.example +2 -0
- package/examples/mastra/README.md +57 -0
- package/examples/mastra/package-lock.json +5296 -0
- package/examples/mastra/package.json +32 -0
- package/examples/mastra/src/cli-markdown.d.ts +137 -0
- package/examples/mastra/src/index.ts +120 -0
- package/examples/mastra/src/mastra/agents/weather-agent.ts +30 -0
- package/examples/mastra/src/mastra/index.ts +21 -0
- package/examples/mastra/src/mastra/tools/weather-tool.ts +102 -0
- package/examples/mastra/tsconfig.json +25 -0
- package/examples/vercel-ai/.env.example +2 -0
- package/examples/vercel-ai/README.md +38 -0
- package/examples/vercel-ai/package-lock.json +2571 -0
- package/examples/vercel-ai/package.json +27 -0
- package/examples/vercel-ai/src/cli-markdown.d.ts +137 -0
- package/examples/vercel-ai/src/index.ts +110 -0
- package/examples/vercel-ai/src/instrumentation.ts +9 -0
- package/examples/vercel-ai/tsconfig.json +25 -0
- package/package.json +80 -33
- package/src/__tests__/client-browser.test.ts +92 -0
- package/src/__tests__/client-node.test.ts +76 -0
- package/src/__tests__/client.test.ts +71 -0
- package/src/__tests__/integration/client-browser.test.ts +46 -0
- package/src/__tests__/integration/client-node.test.ts +46 -0
- package/src/client-browser.ts +70 -0
- package/src/client-node.ts +82 -0
- package/src/client-shared.ts +72 -0
- package/src/client.ts +119 -0
- package/src/evaluation/__tests__/record-evaluation.test.ts +112 -0
- package/src/evaluation/__tests__/run-evaluation.test.ts +171 -0
- package/src/evaluation/index.ts +2 -0
- package/src/evaluation/record-evaluation.ts +101 -0
- package/src/evaluation/run-evaluation.ts +133 -0
- package/src/evaluation/tracer.ts +3 -0
- package/src/evaluation/types.ts +23 -0
- package/src/index.ts +10 -591
- package/src/internal/api/__tests__/errors.test.ts +98 -0
- package/src/internal/api/client.ts +30 -0
- package/src/internal/api/errors.ts +32 -0
- package/src/internal/generated/types/.gitkeep +0 -0
- package/src/observability/__tests__/integration/base.test.ts +74 -0
- package/src/observability/__tests__/integration/browser-setup-ordering.test.ts +60 -0
- package/src/observability/__tests__/integration/complex-nested-spans.test.ts +29 -0
- package/src/observability/__tests__/integration/error-handling.test.ts +24 -0
- package/src/observability/__tests__/integration/langwatch-disabled-otel.test.ts +24 -0
- package/src/observability/__tests__/integration/langwatch-first-then-vercel.test.ts +24 -0
- package/src/observability/__tests__/integration/multiple-setup-attempts.test.ts +27 -0
- package/src/observability/__tests__/integration/otel-ordering.test.ts +27 -0
- package/src/observability/__tests__/integration/vercel-configurations.test.ts +20 -0
- package/src/observability/__tests__/integration/vercel-first-then-langwatch.test.ts +27 -0
- package/src/observability/__tests__/span.test.ts +214 -0
- package/src/observability/__tests__/trace.test.ts +180 -0
- package/src/observability/exporters/index.ts +1 -0
- package/src/observability/exporters/langwatch-exporter.ts +53 -0
- package/src/observability/index.ts +4 -0
- package/src/observability/instrumentation/langchain/__tests__/integration/langchain-chatbot.test.ts +112 -0
- package/src/observability/instrumentation/langchain/__tests__/langchain.test.ts +284 -0
- package/src/observability/instrumentation/langchain/index.ts +624 -0
- package/src/observability/processors/__tests__/filterable-batch-span-exporter.test.ts +98 -0
- package/src/observability/processors/filterable-batch-span-processor.ts +99 -0
- package/src/observability/processors/index.ts +1 -0
- package/src/observability/semconv/attributes.ts +185 -0
- package/src/observability/semconv/events.ts +42 -0
- package/src/observability/semconv/index.ts +16 -0
- package/src/observability/semconv/values.ts +159 -0
- package/src/observability/span.ts +728 -0
- package/src/observability/trace.ts +301 -0
- package/src/prompt/__tests__/prompt.test.ts +139 -0
- package/src/prompt/get-prompt-version.ts +49 -0
- package/src/prompt/get-prompt.ts +44 -0
- package/src/prompt/index.ts +3 -0
- package/src/prompt/prompt.ts +133 -0
- package/src/prompt/service.ts +221 -0
- package/src/prompt/tracer.ts +3 -0
- package/src/prompt/types.ts +0 -0
- package/ts-to-zod.config.js +11 -0
- package/tsconfig.json +3 -9
- package/tsup.config.ts +11 -1
- package/vitest.config.ts +1 -0
- package/dist/chunk-FWBCQQYZ.mjs +0 -711
- package/dist/chunk-FWBCQQYZ.mjs.map +0 -1
- package/dist/index.d.mts +0 -1010
- package/dist/index.d.ts +0 -1010
- package/dist/index.js +0 -27294
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -959
- package/dist/index.mjs.map +0 -1
- package/dist/utils-B0pgWcps.d.mts +0 -303
- package/dist/utils-B0pgWcps.d.ts +0 -303
- package/dist/utils.d.mts +0 -2
- package/dist/utils.d.ts +0 -2
- package/dist/utils.js +0 -703
- package/dist/utils.js.map +0 -1
- package/dist/utils.mjs +0 -11
- package/dist/utils.mjs.map +0 -1
- package/example/.env.example +0 -12
- package/example/.eslintrc.json +0 -26
- package/example/LICENSE +0 -13
- package/example/README.md +0 -12
- package/example/app/(chat)/chat/[id]/page.tsx +0 -60
- package/example/app/(chat)/layout.tsx +0 -14
- package/example/app/(chat)/page.tsx +0 -27
- package/example/app/actions.ts +0 -156
- package/example/app/globals.css +0 -76
- package/example/app/guardrails/page.tsx +0 -26
- package/example/app/langchain/page.tsx +0 -27
- package/example/app/langchain-rag/page.tsx +0 -28
- package/example/app/late-update/page.tsx +0 -27
- package/example/app/layout.tsx +0 -64
- package/example/app/login/actions.ts +0 -71
- package/example/app/login/page.tsx +0 -18
- package/example/app/manual/page.tsx +0 -27
- package/example/app/new/page.tsx +0 -5
- package/example/app/opengraph-image.png +0 -0
- package/example/app/share/[id]/page.tsx +0 -58
- package/example/app/signup/actions.ts +0 -111
- package/example/app/signup/page.tsx +0 -18
- package/example/app/twitter-image.png +0 -0
- package/example/auth.config.ts +0 -42
- package/example/auth.ts +0 -45
- package/example/components/button-scroll-to-bottom.tsx +0 -36
- package/example/components/chat-history.tsx +0 -49
- package/example/components/chat-list.tsx +0 -52
- package/example/components/chat-message-actions.tsx +0 -40
- package/example/components/chat-message.tsx +0 -80
- package/example/components/chat-panel.tsx +0 -139
- package/example/components/chat-share-dialog.tsx +0 -95
- package/example/components/chat.tsx +0 -84
- package/example/components/clear-history.tsx +0 -75
- package/example/components/empty-screen.tsx +0 -38
- package/example/components/external-link.tsx +0 -29
- package/example/components/footer.tsx +0 -19
- package/example/components/header.tsx +0 -114
- package/example/components/login-button.tsx +0 -42
- package/example/components/login-form.tsx +0 -97
- package/example/components/markdown.tsx +0 -9
- package/example/components/prompt-form.tsx +0 -115
- package/example/components/providers.tsx +0 -17
- package/example/components/sidebar-actions.tsx +0 -125
- package/example/components/sidebar-desktop.tsx +0 -19
- package/example/components/sidebar-footer.tsx +0 -16
- package/example/components/sidebar-item.tsx +0 -124
- package/example/components/sidebar-items.tsx +0 -42
- package/example/components/sidebar-list.tsx +0 -38
- package/example/components/sidebar-mobile.tsx +0 -31
- package/example/components/sidebar-toggle.tsx +0 -24
- package/example/components/sidebar.tsx +0 -21
- package/example/components/signup-form.tsx +0 -95
- package/example/components/stocks/events-skeleton.tsx +0 -31
- package/example/components/stocks/events.tsx +0 -30
- package/example/components/stocks/index.tsx +0 -36
- package/example/components/stocks/message.tsx +0 -134
- package/example/components/stocks/spinner.tsx +0 -16
- package/example/components/stocks/stock-purchase.tsx +0 -146
- package/example/components/stocks/stock-skeleton.tsx +0 -22
- package/example/components/stocks/stock.tsx +0 -210
- package/example/components/stocks/stocks-skeleton.tsx +0 -9
- package/example/components/stocks/stocks.tsx +0 -67
- package/example/components/tailwind-indicator.tsx +0 -14
- package/example/components/theme-toggle.tsx +0 -31
- package/example/components/ui/alert-dialog.tsx +0 -141
- package/example/components/ui/badge.tsx +0 -36
- package/example/components/ui/button.tsx +0 -57
- package/example/components/ui/codeblock.tsx +0 -148
- package/example/components/ui/dialog.tsx +0 -122
- package/example/components/ui/dropdown-menu.tsx +0 -205
- package/example/components/ui/icons.tsx +0 -507
- package/example/components/ui/input.tsx +0 -25
- package/example/components/ui/label.tsx +0 -26
- package/example/components/ui/select.tsx +0 -164
- package/example/components/ui/separator.tsx +0 -31
- package/example/components/ui/sheet.tsx +0 -140
- package/example/components/ui/sonner.tsx +0 -31
- package/example/components/ui/switch.tsx +0 -29
- package/example/components/ui/textarea.tsx +0 -24
- package/example/components/ui/tooltip.tsx +0 -30
- package/example/components/user-menu.tsx +0 -53
- package/example/components.json +0 -17
- package/example/instrumentation.ts +0 -11
- package/example/lib/chat/guardrails.tsx +0 -181
- package/example/lib/chat/langchain-rag.tsx +0 -191
- package/example/lib/chat/langchain.tsx +0 -112
- package/example/lib/chat/late-update.tsx +0 -208
- package/example/lib/chat/manual.tsx +0 -605
- package/example/lib/chat/vercel-ai.tsx +0 -576
- package/example/lib/hooks/use-copy-to-clipboard.tsx +0 -33
- package/example/lib/hooks/use-enter-submit.tsx +0 -23
- package/example/lib/hooks/use-local-storage.ts +0 -24
- package/example/lib/hooks/use-scroll-anchor.tsx +0 -86
- package/example/lib/hooks/use-sidebar.tsx +0 -60
- package/example/lib/hooks/use-streamable-text.ts +0 -25
- package/example/lib/types.ts +0 -41
- package/example/lib/utils.ts +0 -89
- package/example/middleware.ts +0 -8
- package/example/next-env.d.ts +0 -5
- package/example/next.config.js +0 -16
- package/example/package-lock.json +0 -9990
- package/example/package.json +0 -84
- package/example/pnpm-lock.yaml +0 -5712
- package/example/postcss.config.js +0 -6
- package/example/prettier.config.cjs +0 -34
- package/example/public/apple-touch-icon.png +0 -0
- package/example/public/favicon-16x16.png +0 -0
- package/example/public/favicon.ico +0 -0
- package/example/public/next.svg +0 -1
- package/example/public/thirteen.svg +0 -1
- package/example/public/vercel.svg +0 -1
- package/example/tailwind.config.ts +0 -81
- package/example/tsconfig.json +0 -35
- package/src/LangWatchExporter.ts +0 -91
- package/src/evaluations.ts +0 -219
- package/src/index.test.ts +0 -402
- package/src/langchain.ts +0 -557
- package/src/typeUtils.ts +0 -89
- package/src/types.ts +0 -79
- package/src/utils.ts +0 -205
- /package/src/{server/types → internal/generated/openapi}/.gitkeep +0 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import openApiCreateClient from "openapi-fetch";
|
|
2
|
+
import type { paths } from "../generated/openapi/api-client";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { getApiKey, getEndpoint } from "../../client";
|
|
5
|
+
|
|
6
|
+
// Define the client type explicitly to avoid naming issues
|
|
7
|
+
export type LangwatchApiClient = ReturnType<typeof openApiCreateClient<paths>>;
|
|
8
|
+
|
|
9
|
+
const configSchema = z.object({
|
|
10
|
+
apiKey: z.string().min(1, "API key is required"),
|
|
11
|
+
endpoint: z.string().url("Endpoint must be a valid URL"),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export function createLangWatchApiClient(apiKey?: string | undefined, endpoint?: string | undefined ): LangwatchApiClient {
|
|
15
|
+
// This will error if the config is invalid
|
|
16
|
+
const config = configSchema.parse({
|
|
17
|
+
apiKey: apiKey ?? getApiKey(),
|
|
18
|
+
endpoint: endpoint ?? getEndpoint(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return openApiCreateClient<paths>({
|
|
22
|
+
baseUrl: config.endpoint,
|
|
23
|
+
headers: {
|
|
24
|
+
"X-Auth-Token": config.apiKey,
|
|
25
|
+
"Content-Type": "application/json",
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export class LangWatchApiError extends Error {
|
|
2
|
+
public readonly httpStatus: number;
|
|
3
|
+
public readonly httpStatusText: string;
|
|
4
|
+
public apiError: string | undefined;
|
|
5
|
+
public body: unknown;
|
|
6
|
+
|
|
7
|
+
constructor(message: string, response: Response) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.httpStatus = response.status;
|
|
10
|
+
this.httpStatusText = response.statusText;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async safeParseBody(response: Response): Promise<void> {
|
|
14
|
+
try {
|
|
15
|
+
if (response.headers.get("Content-Type")?.includes("application/json")) {
|
|
16
|
+
const json = await response.json();
|
|
17
|
+
|
|
18
|
+
this.body = json;
|
|
19
|
+
|
|
20
|
+
if (json.error && typeof json.error === "string") {
|
|
21
|
+
this.apiError = json.error;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.body = await response.text();
|
|
28
|
+
} catch {
|
|
29
|
+
this.body = null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import { setupLangWatch } from "../../../client-node";
|
|
3
|
+
import { getLangWatchTracer } from "../../trace";
|
|
4
|
+
|
|
5
|
+
const tracerName = "basic-observability.test";
|
|
6
|
+
|
|
7
|
+
describe("basic observability tests around tracing", () => {
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
await setupLangWatch();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("traces should be sent", async () => {
|
|
13
|
+
const tracer = getLangWatchTracer(tracerName);
|
|
14
|
+
await tracer.withActiveSpan(
|
|
15
|
+
"basic trace",
|
|
16
|
+
async () => { },
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("traces should be sent with complex arguments", async () => {
|
|
21
|
+
const tracer = getLangWatchTracer(tracerName);
|
|
22
|
+
await tracer.withActiveSpan(
|
|
23
|
+
"complex argument trace",
|
|
24
|
+
{ attributes: { foo: "bar" }, root: true },
|
|
25
|
+
async (span) => {
|
|
26
|
+
span.setAttributes({
|
|
27
|
+
bar: "bas",
|
|
28
|
+
});
|
|
29
|
+
span.addEvent("test event", {
|
|
30
|
+
foo: "bar",
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("traces exceptions", async () => {
|
|
37
|
+
const tracer = getLangWatchTracer(tracerName);
|
|
38
|
+
|
|
39
|
+
await expect(
|
|
40
|
+
tracer.withActiveSpan(
|
|
41
|
+
"trace exception",
|
|
42
|
+
async () => {
|
|
43
|
+
throw new Error("this is meant to error");
|
|
44
|
+
},
|
|
45
|
+
)
|
|
46
|
+
).rejects.toThrow("this is meant to error");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("traces handle complex nesting", async () => {
|
|
50
|
+
const tracer = getLangWatchTracer(tracerName);
|
|
51
|
+
await tracer.withActiveSpan(
|
|
52
|
+
"complex nesting trace",
|
|
53
|
+
async () => {
|
|
54
|
+
await tracer.withActiveSpan(
|
|
55
|
+
"nested trace alpha",
|
|
56
|
+
async () => { },
|
|
57
|
+
);
|
|
58
|
+
await tracer.withActiveSpan(
|
|
59
|
+
"nested trace beta",
|
|
60
|
+
async () => { },
|
|
61
|
+
);
|
|
62
|
+
await tracer.withActiveSpan(
|
|
63
|
+
"nested trace gamma",
|
|
64
|
+
async () => {
|
|
65
|
+
await tracer.withActiveSpan(
|
|
66
|
+
"nested trace gamma child",
|
|
67
|
+
async () => { },
|
|
68
|
+
);
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { setupLangWatch as setupBrowser } from "../../../client-browser";
|
|
3
|
+
import { getLangWatchTracer } from "../../trace";
|
|
4
|
+
|
|
5
|
+
// Mock window object for Node.js test environment
|
|
6
|
+
const mockWindow = {
|
|
7
|
+
addEventListener: vi.fn(),
|
|
8
|
+
removeEventListener: vi.fn(),
|
|
9
|
+
};
|
|
10
|
+
global.window = mockWindow as any;
|
|
11
|
+
|
|
12
|
+
describe("Browser SDK setup ordering", () => {
|
|
13
|
+
it("should work when LangWatch browser setup is called multiple times", async () => {
|
|
14
|
+
// First setup
|
|
15
|
+
await setupBrowser({
|
|
16
|
+
apiKey: "test-key-1",
|
|
17
|
+
endpoint: "http://localhost:9999",
|
|
18
|
+
skipOpenTelemetrySetup: false,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Second setup should work (browser doesn't have the same restriction as node)
|
|
22
|
+
await setupBrowser({
|
|
23
|
+
apiKey: "test-key-2",
|
|
24
|
+
endpoint: "http://localhost:9999",
|
|
25
|
+
skipOpenTelemetrySetup: false,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Test that LangWatch still works
|
|
29
|
+
const tracer = getLangWatchTracer("browser-multiple-test");
|
|
30
|
+
await tracer.withActiveSpan("test span", async () => {
|
|
31
|
+
expect(true).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should work when LangWatch is set up with skipOpenTelemetrySetup=true", async () => {
|
|
36
|
+
// Setup LangWatch with automatic setup disabled
|
|
37
|
+
await setupBrowser({
|
|
38
|
+
apiKey: "test-key",
|
|
39
|
+
endpoint: "http://localhost:9999",
|
|
40
|
+
skipOpenTelemetrySetup: true,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Test that LangWatch still works
|
|
44
|
+
const tracer = getLangWatchTracer("browser-disabled-test");
|
|
45
|
+
await tracer.withActiveSpan("test span", async () => {
|
|
46
|
+
expect(true).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should handle window event listeners correctly", async () => {
|
|
51
|
+
await setupBrowser({
|
|
52
|
+
apiKey: "test-key",
|
|
53
|
+
endpoint: "http://localhost:9999",
|
|
54
|
+
skipOpenTelemetrySetup: false,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Check that window event listeners were added
|
|
58
|
+
expect(mockWindow.addEventListener).toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { registerOTel } from '@vercel/otel';
|
|
3
|
+
import { setupLangWatch } from "../../../client-node";
|
|
4
|
+
import { getLangWatchTracer } from "../../trace";
|
|
5
|
+
|
|
6
|
+
describe("Complex nested spans with Vercel AI", () => {
|
|
7
|
+
it("should work with complex nested spans when Vercel AI is set up first", async () => {
|
|
8
|
+
registerOTel({ serviceName: 'complex-test' });
|
|
9
|
+
await setupLangWatch({
|
|
10
|
+
apiKey: "test-key",
|
|
11
|
+
endpoint: "http://localhost:9999",
|
|
12
|
+
skipOpenTelemetrySetup: false,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const tracer = getLangWatchTracer("complex-otel-test");
|
|
16
|
+
|
|
17
|
+
await tracer.withActiveSpan("root span", async () => {
|
|
18
|
+
await tracer.withActiveSpan("child span 1", async () => {
|
|
19
|
+
await tracer.withActiveSpan("grandchild span", async () => {
|
|
20
|
+
expect(true).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await tracer.withActiveSpan("child span 2", async () => {
|
|
25
|
+
expect(true).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { registerOTel } from '@vercel/otel';
|
|
3
|
+
import { setupLangWatch } from "../../../client-node";
|
|
4
|
+
import { getLangWatchTracer } from "../../trace";
|
|
5
|
+
|
|
6
|
+
describe("Error handling with Vercel AI", () => {
|
|
7
|
+
it("should handle errors gracefully when both are set up", async () => {
|
|
8
|
+
registerOTel({ serviceName: 'error-test' });
|
|
9
|
+
await setupLangWatch({
|
|
10
|
+
apiKey: "test-key",
|
|
11
|
+
endpoint: "http://localhost:9999",
|
|
12
|
+
skipOpenTelemetrySetup: false,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const tracer = getLangWatchTracer("error-otel-test");
|
|
16
|
+
|
|
17
|
+
// Test that exceptions in spans are properly handled
|
|
18
|
+
await expect(
|
|
19
|
+
tracer.withActiveSpan("error span", async () => {
|
|
20
|
+
throw new Error("Test error");
|
|
21
|
+
})
|
|
22
|
+
).rejects.toThrow("Test error");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { registerOTel } from '@vercel/otel';
|
|
3
|
+
import { setupLangWatch } from "../../../client-node";
|
|
4
|
+
import { getLangWatchTracer } from "../../trace";
|
|
5
|
+
|
|
6
|
+
describe("LangWatch with skipOpenTelemetrySetup=true", () => {
|
|
7
|
+
it("should work when LangWatch is set up with skipOpenTelemetrySetup=true", async () => {
|
|
8
|
+
// Setup Vercel AI first
|
|
9
|
+
registerOTel({ serviceName: 'vercel-first' });
|
|
10
|
+
|
|
11
|
+
// Then setup LangWatch with automatic setup disabled
|
|
12
|
+
await setupLangWatch({
|
|
13
|
+
apiKey: "test-key",
|
|
14
|
+
endpoint: "http://localhost:9999",
|
|
15
|
+
skipOpenTelemetrySetup: true,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Test that LangWatch still works
|
|
19
|
+
const tracer = getLangWatchTracer("disabled-otel-test");
|
|
20
|
+
await tracer.withActiveSpan("test span", async () => {
|
|
21
|
+
expect(true).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { registerOTel } from '@vercel/otel';
|
|
3
|
+
import { setupLangWatch } from "../../../client-node";
|
|
4
|
+
import { getLangWatchTracer } from "../../trace";
|
|
5
|
+
|
|
6
|
+
describe("LangWatch setup first, then Vercel AI", () => {
|
|
7
|
+
it("should work when LangWatch is set up first, then Vercel AI", async () => {
|
|
8
|
+
// Setup LangWatch first
|
|
9
|
+
await setupLangWatch({
|
|
10
|
+
apiKey: "test-key",
|
|
11
|
+
endpoint: "http://localhost:9999",
|
|
12
|
+
skipOpenTelemetrySetup: false,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Then setup Vercel AI
|
|
16
|
+
registerOTel({ serviceName: 'langwatch-first' });
|
|
17
|
+
|
|
18
|
+
// Test that LangWatch still works
|
|
19
|
+
const tracer = getLangWatchTracer("langwatch-first-test");
|
|
20
|
+
await tracer.withActiveSpan("test span", async () => {
|
|
21
|
+
expect(true).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { setupLangWatch } from "../../../client-node";
|
|
3
|
+
import { getLangWatchTracer } from "../../trace";
|
|
4
|
+
|
|
5
|
+
describe("Multiple setup attempts and error handling", () => {
|
|
6
|
+
it("should throw error when multiple LangWatch setups are called", async () => {
|
|
7
|
+
// First setup
|
|
8
|
+
await setupLangWatch({
|
|
9
|
+
apiKey: "test-key-1",
|
|
10
|
+
endpoint: "http://localhost:9999",
|
|
11
|
+
skipOpenTelemetrySetup: false,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Second setup should throw an error
|
|
15
|
+
await expect(setupLangWatch({
|
|
16
|
+
apiKey: "test-key-2",
|
|
17
|
+
endpoint: "http://localhost:9999",
|
|
18
|
+
skipOpenTelemetrySetup: false,
|
|
19
|
+
})).rejects.toThrow("LangWatch setup has already been called");
|
|
20
|
+
|
|
21
|
+
// Test that LangWatch still works
|
|
22
|
+
const tracer = getLangWatchTracer("multiple-setup-test");
|
|
23
|
+
await tracer.withActiveSpan("test span", async () => {
|
|
24
|
+
expect(true).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { registerOTel } from '@vercel/otel'
|
|
3
|
+
import { getLangWatchTracer } from "../../trace";
|
|
4
|
+
import { setupLangWatch } from "../../../client-node";
|
|
5
|
+
import { isOtelInitialized } from "../../../client-shared";
|
|
6
|
+
|
|
7
|
+
describe("SDK compatibility with Vercel AI OpenTelemetry", () => {
|
|
8
|
+
it("should work when Vercel AI is set up first, then LangWatch", async () => {
|
|
9
|
+
// Setup Vercel AI first
|
|
10
|
+
registerOTel({ serviceName: 'vercel-first' });
|
|
11
|
+
|
|
12
|
+
expect(isOtelInitialized()).toBe(true);
|
|
13
|
+
|
|
14
|
+
// Then setup LangWatch
|
|
15
|
+
await setupLangWatch({
|
|
16
|
+
apiKey: "test-key",
|
|
17
|
+
endpoint: "http://localhost:9999",
|
|
18
|
+
skipOpenTelemetrySetup: false,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Test that LangWatch still works
|
|
22
|
+
const tracer = getLangWatchTracer("vercel-first-test");
|
|
23
|
+
await tracer.withActiveSpan("test span", async () => {
|
|
24
|
+
expect(true).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { registerOTel } from '@vercel/otel';
|
|
3
|
+
import { setupLangWatch } from "../../../client-node";
|
|
4
|
+
import { getLangWatchTracer } from "../../trace";
|
|
5
|
+
|
|
6
|
+
describe("Different Vercel AI configurations", () => {
|
|
7
|
+
it("should work with a specific Vercel AI configuration", async () => {
|
|
8
|
+
registerOTel({ serviceName: 'config-1', version: '1.0.0' });
|
|
9
|
+
await setupLangWatch({
|
|
10
|
+
apiKey: "test-key",
|
|
11
|
+
endpoint: "http://localhost:9999",
|
|
12
|
+
skipOpenTelemetrySetup: false,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const tracer = getLangWatchTracer("config-test");
|
|
16
|
+
await tracer.withActiveSpan("test span", async () => {
|
|
17
|
+
expect(true).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { registerOTel } from '@vercel/otel';
|
|
3
|
+
import { setupLangWatch } from "../../../client-node";
|
|
4
|
+
import { getLangWatchTracer } from "../../trace";
|
|
5
|
+
import { isOtelInitialized } from "../../../client-shared";
|
|
6
|
+
|
|
7
|
+
describe("Vercel AI setup first, then LangWatch", () => {
|
|
8
|
+
it("should work when Vercel AI is set up first, then LangWatch", async () => {
|
|
9
|
+
// Setup Vercel AI first
|
|
10
|
+
registerOTel({ serviceName: 'vercel-first' });
|
|
11
|
+
|
|
12
|
+
expect(isOtelInitialized()).toBe(true);
|
|
13
|
+
|
|
14
|
+
// Then setup LangWatch
|
|
15
|
+
await setupLangWatch({
|
|
16
|
+
apiKey: "test-key",
|
|
17
|
+
endpoint: "http://localhost:9999",
|
|
18
|
+
skipOpenTelemetrySetup: false,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Test that LangWatch still works
|
|
22
|
+
const tracer = getLangWatchTracer("vercel-first-test");
|
|
23
|
+
await tracer.withActiveSpan("test span", async () => {
|
|
24
|
+
expect(true).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
vi.mock('../../evaluation/record-evaluation', () => ({
|
|
2
|
+
recordEvaluation: vi.fn(),
|
|
3
|
+
}));
|
|
4
|
+
|
|
5
|
+
import { createLangWatchSpan } from '../span';
|
|
6
|
+
import { recordEvaluation } from '../../evaluation/record-evaluation';
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
9
|
+
|
|
10
|
+
const makeMockSpan = () => {
|
|
11
|
+
const calls: any = { addEvent: [], setAttribute: [], end: 0, setAttributes: [], recordException: [], setStatus: [], updateName: [] };
|
|
12
|
+
return {
|
|
13
|
+
addEvent: vi.fn((...args) => { calls.addEvent.push(args); }),
|
|
14
|
+
setAttribute: vi.fn((...args) => { calls.setAttribute.push(args); }),
|
|
15
|
+
setAttributes: vi.fn((...args) => { calls.setAttributes.push(args); }),
|
|
16
|
+
end: vi.fn(() => { calls.end++; }),
|
|
17
|
+
recordException: vi.fn((...args) => { calls.recordException.push(args); }),
|
|
18
|
+
setStatus: vi.fn((...args) => { calls.setStatus.push(args); }),
|
|
19
|
+
updateName: vi.fn((...args) => { calls.updateName.push(args); }),
|
|
20
|
+
calls,
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe('createLangWatchSpan', () => {
|
|
25
|
+
let span: ReturnType<typeof makeMockSpan>;
|
|
26
|
+
let lwSpan: ReturnType<typeof createLangWatchSpan>;
|
|
27
|
+
let intSemconv: any;
|
|
28
|
+
let semconv: any;
|
|
29
|
+
|
|
30
|
+
beforeEach(async () => {
|
|
31
|
+
span = makeMockSpan();
|
|
32
|
+
lwSpan = createLangWatchSpan(span as any);
|
|
33
|
+
intSemconv = await import('../semconv/index.js');
|
|
34
|
+
semconv = await import('@opentelemetry/semantic-conventions/incubating');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('setType sets the span type attribute', () => {
|
|
38
|
+
lwSpan.setType('llm');
|
|
39
|
+
expect(span.setAttribute).toHaveBeenCalledWith(intSemconv.ATTR_LANGWATCH_SPAN_TYPE, 'llm');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('setRequestModel sets the request model attribute', () => {
|
|
43
|
+
lwSpan.setRequestModel('gpt-4');
|
|
44
|
+
expect(span.setAttribute).toHaveBeenCalledWith(semconv.ATTR_GEN_AI_REQUEST_MODEL, 'gpt-4');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('setResponseModel sets the response model attribute', () => {
|
|
48
|
+
lwSpan.setResponseModel('gpt-4');
|
|
49
|
+
expect(span.setAttribute).toHaveBeenCalledWith(semconv.ATTR_GEN_AI_RESPONSE_MODEL, 'gpt-4');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('setRAGContexts sets the rag contexts attribute as JSON', () => {
|
|
53
|
+
const ctxs = [{ document_id: 'd', chunk_id: 'c', content: 'x' }];
|
|
54
|
+
lwSpan.setRAGContexts(ctxs);
|
|
55
|
+
expect(span.setAttribute).toHaveBeenCalledWith(intSemconv.ATTR_LANGWATCH_RAG_CONTEXTS, JSON.stringify({ type: 'json', value: ctxs }));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('setRAGContext sets a single rag context as JSON array', () => {
|
|
59
|
+
const ctx = { document_id: 'd', chunk_id: 'c', content: 'x' };
|
|
60
|
+
lwSpan.setRAGContext(ctx);
|
|
61
|
+
expect(span.setAttribute).toHaveBeenCalledWith(intSemconv.ATTR_LANGWATCH_RAG_CONTEXTS, JSON.stringify({ type: 'json', value: [ctx] }));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('setMetrics sets the metrics attribute as JSON', () => {
|
|
65
|
+
const metrics = { promptTokens: 1, completionTokens: 2, cost: 3 };
|
|
66
|
+
lwSpan.setMetrics(metrics);
|
|
67
|
+
expect(span.setAttribute).toHaveBeenCalledWith(intSemconv.ATTR_LANGWATCH_METRICS, JSON.stringify({ type: 'json', value: metrics }));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('setInput sets the input attribute as JSON', () => {
|
|
71
|
+
lwSpan.setInput({ foo: 'bar' });
|
|
72
|
+
expect(span.setAttribute).toHaveBeenCalledWith(intSemconv.ATTR_LANGWATCH_INPUT, JSON.stringify({ type: 'json', value: { foo: 'bar' } }));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('setInputString sets the input attribute as text', () => {
|
|
76
|
+
lwSpan.setInputString('prompt');
|
|
77
|
+
expect(span.setAttribute).toHaveBeenCalledWith(intSemconv.ATTR_LANGWATCH_INPUT, JSON.stringify({ type: 'text', value: 'prompt' }));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('setOutput sets the output attribute as JSON', () => {
|
|
81
|
+
lwSpan.setOutput({ foo: 'bar' });
|
|
82
|
+
expect(span.setAttribute).toHaveBeenCalledWith(intSemconv.ATTR_LANGWATCH_OUTPUT, JSON.stringify({ type: 'json', value: { foo: 'bar' } }));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('setOutputString sets the output attribute as text', () => {
|
|
86
|
+
lwSpan.setOutputString('completion');
|
|
87
|
+
expect(span.setAttribute).toHaveBeenCalledWith(intSemconv.ATTR_LANGWATCH_OUTPUT, JSON.stringify({ type: 'text', value: 'completion' }));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('setOutputEvaluation sets the output attribute as guardrail/evaluation result', () => {
|
|
91
|
+
lwSpan.setOutputEvaluation(true, { status: 'processed', passed: true });
|
|
92
|
+
expect(span.setAttribute).toHaveBeenCalledWith(intSemconv.ATTR_LANGWATCH_OUTPUT, JSON.stringify({ type: 'guardrail_result', value: { status: 'processed', passed: true } }));
|
|
93
|
+
lwSpan.setOutputEvaluation(false, { status: 'processed', passed: false });
|
|
94
|
+
expect(span.setAttribute).toHaveBeenCalledWith(intSemconv.ATTR_LANGWATCH_OUTPUT, JSON.stringify({ type: 'evaluation_result', value: { status: 'processed', passed: false } }));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('addGenAISystemMessageEvent sets default role and adds event', () => {
|
|
98
|
+
lwSpan.addGenAISystemMessageEvent({ content: 'hi' });
|
|
99
|
+
expect(span.addEvent).toHaveBeenCalledWith(intSemconv.LOG_EVNT_GEN_AI_SYSTEM_MESSAGE, expect.objectContaining({
|
|
100
|
+
[semconv.ATTR_GEN_AI_SYSTEM]: undefined,
|
|
101
|
+
[intSemconv.ATTR_LANGWATCH_GEN_AI_LOG_EVENT_BODY]: JSON.stringify({ content: 'hi', role: 'system' }),
|
|
102
|
+
[intSemconv.ATTR_LANGWATCH_GEN_AI_LOG_EVENT_IMPOSTER]: true,
|
|
103
|
+
}));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('addGenAIUserMessageEvent sets default role and adds event', () => {
|
|
107
|
+
lwSpan.addGenAIUserMessageEvent({ content: 'hi' });
|
|
108
|
+
expect(span.addEvent).toHaveBeenCalledWith(intSemconv.LOG_EVNT_GEN_AI_USER_MESSAGE, expect.objectContaining({
|
|
109
|
+
[semconv.ATTR_GEN_AI_SYSTEM]: undefined,
|
|
110
|
+
[intSemconv.ATTR_LANGWATCH_GEN_AI_LOG_EVENT_BODY]: JSON.stringify({ content: 'hi', role: 'user' }),
|
|
111
|
+
[intSemconv.ATTR_LANGWATCH_GEN_AI_LOG_EVENT_IMPOSTER]: true,
|
|
112
|
+
}));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('addGenAIAssistantMessageEvent sets default role and adds event', () => {
|
|
116
|
+
lwSpan.addGenAIAssistantMessageEvent({ content: 'hi' });
|
|
117
|
+
expect(span.addEvent).toHaveBeenCalledWith(intSemconv.LOG_EVNT_GEN_AI_ASSISTANT_MESSAGE, expect.objectContaining({
|
|
118
|
+
[semconv.ATTR_GEN_AI_SYSTEM]: undefined,
|
|
119
|
+
[intSemconv.ATTR_LANGWATCH_GEN_AI_LOG_EVENT_BODY]: JSON.stringify({ content: 'hi', role: 'assistant' }),
|
|
120
|
+
[intSemconv.ATTR_LANGWATCH_GEN_AI_LOG_EVENT_IMPOSTER]: true,
|
|
121
|
+
}));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('addGenAIToolMessageEvent sets default role and adds event', () => {
|
|
125
|
+
lwSpan.addGenAIToolMessageEvent({ content: 'hi', id: 't1' });
|
|
126
|
+
expect(span.addEvent).toHaveBeenCalledWith(intSemconv.LOG_EVNT_GEN_AI_TOOL_MESSAGE, expect.objectContaining({
|
|
127
|
+
[semconv.ATTR_GEN_AI_SYSTEM]: undefined,
|
|
128
|
+
[intSemconv.ATTR_LANGWATCH_GEN_AI_LOG_EVENT_BODY]: JSON.stringify({ content: 'hi', id: 't1', role: 'tool' }),
|
|
129
|
+
[intSemconv.ATTR_LANGWATCH_GEN_AI_LOG_EVENT_IMPOSTER]: true,
|
|
130
|
+
}));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('addGenAIChoiceEvent sets default message.role and adds event', () => {
|
|
134
|
+
lwSpan.addGenAIChoiceEvent({ finish_reason: 'stop', index: 0, message: { content: 'x' } });
|
|
135
|
+
expect(span.addEvent).toHaveBeenCalledWith(intSemconv.LOG_EVNT_GEN_AI_CHOICE, expect.objectContaining({
|
|
136
|
+
[semconv.ATTR_GEN_AI_SYSTEM]: undefined,
|
|
137
|
+
[intSemconv.ATTR_LANGWATCH_GEN_AI_LOG_EVENT_BODY]: JSON.stringify({ finish_reason: 'stop', index: 0, message: { content: 'x', role: 'assistant' } }),
|
|
138
|
+
[intSemconv.ATTR_LANGWATCH_GEN_AI_LOG_EVENT_IMPOSTER]: true,
|
|
139
|
+
}));
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('recordEvaluation calls recordEvaluation util', () => {
|
|
143
|
+
const details = { name: 'eval', status: 'processed' as const };
|
|
144
|
+
const attributes = { foo: 'bar' };
|
|
145
|
+
const span = makeMockSpan();
|
|
146
|
+
const lwSpan2 = createLangWatchSpan(span as any);
|
|
147
|
+
lwSpan2.recordEvaluation(details, attributes);
|
|
148
|
+
expect(recordEvaluation).toHaveBeenCalledWith(details, attributes);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('supports fluent API chaining', () => {
|
|
152
|
+
// Test that methods can be chained and each returns the LangWatchSpan instance
|
|
153
|
+
const result = lwSpan
|
|
154
|
+
.setType('llm')
|
|
155
|
+
.setRequestModel('gpt-4')
|
|
156
|
+
.setInputString('Hello')
|
|
157
|
+
.addGenAIUserMessageEvent({ content: 'Hello' })
|
|
158
|
+
.addGenAIAssistantMessageEvent({ content: 'Hi!' })
|
|
159
|
+
.setOutputString('Hi!')
|
|
160
|
+
.recordEvaluation({ name: 'test', status: 'processed' });
|
|
161
|
+
|
|
162
|
+
// Verify that the result is the same LangWatchSpan instance
|
|
163
|
+
expect(result).toBe(lwSpan);
|
|
164
|
+
|
|
165
|
+
// Verify that all the expected methods were called
|
|
166
|
+
expect(span.setAttribute).toHaveBeenCalledWith(intSemconv.ATTR_LANGWATCH_SPAN_TYPE, 'llm');
|
|
167
|
+
expect(span.setAttribute).toHaveBeenCalledWith(semconv.ATTR_GEN_AI_REQUEST_MODEL, 'gpt-4');
|
|
168
|
+
expect(span.setAttribute).toHaveBeenCalledWith(intSemconv.ATTR_LANGWATCH_INPUT, JSON.stringify({ type: 'text', value: 'Hello' }));
|
|
169
|
+
expect(span.setAttribute).toHaveBeenCalledWith(intSemconv.ATTR_LANGWATCH_OUTPUT, JSON.stringify({ type: 'text', value: 'Hi!' }));
|
|
170
|
+
expect(span.addEvent).toHaveBeenCalledWith(intSemconv.LOG_EVNT_GEN_AI_USER_MESSAGE, expect.any(Object));
|
|
171
|
+
expect(span.addEvent).toHaveBeenCalledWith(intSemconv.LOG_EVNT_GEN_AI_ASSISTANT_MESSAGE, expect.any(Object));
|
|
172
|
+
expect(recordEvaluation).toHaveBeenCalledWith({ name: 'test', status: 'processed' }, undefined);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('maintains fluent API when mixing LangWatch and original span methods', () => {
|
|
176
|
+
// Test that we can chain LangWatch methods with original span methods
|
|
177
|
+
const result = lwSpan
|
|
178
|
+
.setType('llm')
|
|
179
|
+
.setAttribute('custom.attr', 'value') // Original span method
|
|
180
|
+
.setInputString('Hello')
|
|
181
|
+
.setAttributes({ 'another.attr': 'value' }) // Original span method
|
|
182
|
+
.setOutputString('Hi!');
|
|
183
|
+
|
|
184
|
+
// Verify that the result is the same LangWatchSpan instance
|
|
185
|
+
expect(result).toBe(lwSpan);
|
|
186
|
+
|
|
187
|
+
// Verify that both LangWatch and original span methods were called
|
|
188
|
+
expect(span.setAttribute).toHaveBeenCalledWith(intSemconv.ATTR_LANGWATCH_SPAN_TYPE, 'llm');
|
|
189
|
+
expect(span.setAttribute).toHaveBeenCalledWith('custom.attr', 'value');
|
|
190
|
+
expect(span.setAttributes).toHaveBeenCalledWith({ 'another.attr': 'value' });
|
|
191
|
+
expect(span.setAttribute).toHaveBeenCalledWith(intSemconv.ATTR_LANGWATCH_INPUT, JSON.stringify({ type: 'text', value: 'Hello' }));
|
|
192
|
+
expect(span.setAttribute).toHaveBeenCalledWith(intSemconv.ATTR_LANGWATCH_OUTPUT, JSON.stringify({ type: 'text', value: 'Hi!' }));
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('forwards original span methods correctly', () => {
|
|
196
|
+
// Test that original span methods work and return the LangWatchSpan for chaining
|
|
197
|
+
const result = lwSpan
|
|
198
|
+
.setAttribute('test', 'value')
|
|
199
|
+
.setAttributes({ 'test2': 'value2' })
|
|
200
|
+
.addEvent('test-event', { 'event-attr': 'value' })
|
|
201
|
+
.setStatus({ code: 0 })
|
|
202
|
+
.updateName('new-name');
|
|
203
|
+
|
|
204
|
+
// Verify that the result is the same LangWatchSpan instance
|
|
205
|
+
expect(result).toBe(lwSpan);
|
|
206
|
+
|
|
207
|
+
// Verify that all the original span methods were called
|
|
208
|
+
expect(span.setAttribute).toHaveBeenCalledWith('test', 'value');
|
|
209
|
+
expect(span.setAttributes).toHaveBeenCalledWith({ 'test2': 'value2' });
|
|
210
|
+
expect(span.addEvent).toHaveBeenCalledWith('test-event', { 'event-attr': 'value' });
|
|
211
|
+
expect(span.setStatus).toHaveBeenCalledWith({ code: 0 });
|
|
212
|
+
expect(span.updateName).toHaveBeenCalledWith('new-name');
|
|
213
|
+
});
|
|
214
|
+
});
|