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.
Files changed (235) hide show
  1. package/.editorconfig +16 -0
  2. package/LICENSE +7 -0
  3. package/README.md +268 -1
  4. package/copy-types.sh +19 -8
  5. package/examples/langchain/.env.example +2 -0
  6. package/examples/langchain/README.md +42 -0
  7. package/examples/langchain/package-lock.json +2930 -0
  8. package/examples/langchain/package.json +27 -0
  9. package/examples/langchain/src/cli-markdown.d.ts +137 -0
  10. package/examples/langchain/src/index.ts +109 -0
  11. package/examples/langchain/tsconfig.json +25 -0
  12. package/examples/langgraph/.env.example +2 -0
  13. package/examples/langgraph/README.md +42 -0
  14. package/examples/langgraph/package-lock.json +3031 -0
  15. package/examples/langgraph/package.json +28 -0
  16. package/examples/langgraph/src/cli-markdown.d.ts +137 -0
  17. package/examples/langgraph/src/index.ts +196 -0
  18. package/examples/langgraph/tsconfig.json +25 -0
  19. package/examples/mastra/.env.example +2 -0
  20. package/examples/mastra/README.md +57 -0
  21. package/examples/mastra/package-lock.json +5296 -0
  22. package/examples/mastra/package.json +32 -0
  23. package/examples/mastra/src/cli-markdown.d.ts +137 -0
  24. package/examples/mastra/src/index.ts +120 -0
  25. package/examples/mastra/src/mastra/agents/weather-agent.ts +30 -0
  26. package/examples/mastra/src/mastra/index.ts +21 -0
  27. package/examples/mastra/src/mastra/tools/weather-tool.ts +102 -0
  28. package/examples/mastra/tsconfig.json +25 -0
  29. package/examples/vercel-ai/.env.example +2 -0
  30. package/examples/vercel-ai/README.md +38 -0
  31. package/examples/vercel-ai/package-lock.json +2571 -0
  32. package/examples/vercel-ai/package.json +27 -0
  33. package/examples/vercel-ai/src/cli-markdown.d.ts +137 -0
  34. package/examples/vercel-ai/src/index.ts +110 -0
  35. package/examples/vercel-ai/src/instrumentation.ts +9 -0
  36. package/examples/vercel-ai/tsconfig.json +25 -0
  37. package/package.json +80 -33
  38. package/src/__tests__/client-browser.test.ts +92 -0
  39. package/src/__tests__/client-node.test.ts +76 -0
  40. package/src/__tests__/client.test.ts +71 -0
  41. package/src/__tests__/integration/client-browser.test.ts +46 -0
  42. package/src/__tests__/integration/client-node.test.ts +46 -0
  43. package/src/client-browser.ts +70 -0
  44. package/src/client-node.ts +82 -0
  45. package/src/client-shared.ts +72 -0
  46. package/src/client.ts +119 -0
  47. package/src/evaluation/__tests__/record-evaluation.test.ts +112 -0
  48. package/src/evaluation/__tests__/run-evaluation.test.ts +171 -0
  49. package/src/evaluation/index.ts +2 -0
  50. package/src/evaluation/record-evaluation.ts +101 -0
  51. package/src/evaluation/run-evaluation.ts +133 -0
  52. package/src/evaluation/tracer.ts +3 -0
  53. package/src/evaluation/types.ts +23 -0
  54. package/src/index.ts +10 -591
  55. package/src/internal/api/__tests__/errors.test.ts +98 -0
  56. package/src/internal/api/client.ts +30 -0
  57. package/src/internal/api/errors.ts +32 -0
  58. package/src/internal/generated/types/.gitkeep +0 -0
  59. package/src/observability/__tests__/integration/base.test.ts +74 -0
  60. package/src/observability/__tests__/integration/browser-setup-ordering.test.ts +60 -0
  61. package/src/observability/__tests__/integration/complex-nested-spans.test.ts +29 -0
  62. package/src/observability/__tests__/integration/error-handling.test.ts +24 -0
  63. package/src/observability/__tests__/integration/langwatch-disabled-otel.test.ts +24 -0
  64. package/src/observability/__tests__/integration/langwatch-first-then-vercel.test.ts +24 -0
  65. package/src/observability/__tests__/integration/multiple-setup-attempts.test.ts +27 -0
  66. package/src/observability/__tests__/integration/otel-ordering.test.ts +27 -0
  67. package/src/observability/__tests__/integration/vercel-configurations.test.ts +20 -0
  68. package/src/observability/__tests__/integration/vercel-first-then-langwatch.test.ts +27 -0
  69. package/src/observability/__tests__/span.test.ts +214 -0
  70. package/src/observability/__tests__/trace.test.ts +180 -0
  71. package/src/observability/exporters/index.ts +1 -0
  72. package/src/observability/exporters/langwatch-exporter.ts +53 -0
  73. package/src/observability/index.ts +4 -0
  74. package/src/observability/instrumentation/langchain/__tests__/integration/langchain-chatbot.test.ts +112 -0
  75. package/src/observability/instrumentation/langchain/__tests__/langchain.test.ts +284 -0
  76. package/src/observability/instrumentation/langchain/index.ts +624 -0
  77. package/src/observability/processors/__tests__/filterable-batch-span-exporter.test.ts +98 -0
  78. package/src/observability/processors/filterable-batch-span-processor.ts +99 -0
  79. package/src/observability/processors/index.ts +1 -0
  80. package/src/observability/semconv/attributes.ts +185 -0
  81. package/src/observability/semconv/events.ts +42 -0
  82. package/src/observability/semconv/index.ts +16 -0
  83. package/src/observability/semconv/values.ts +159 -0
  84. package/src/observability/span.ts +728 -0
  85. package/src/observability/trace.ts +301 -0
  86. package/src/prompt/__tests__/prompt.test.ts +139 -0
  87. package/src/prompt/get-prompt-version.ts +49 -0
  88. package/src/prompt/get-prompt.ts +44 -0
  89. package/src/prompt/index.ts +3 -0
  90. package/src/prompt/prompt.ts +133 -0
  91. package/src/prompt/service.ts +221 -0
  92. package/src/prompt/tracer.ts +3 -0
  93. package/src/prompt/types.ts +0 -0
  94. package/ts-to-zod.config.js +11 -0
  95. package/tsconfig.json +3 -9
  96. package/tsup.config.ts +11 -1
  97. package/vitest.config.ts +1 -0
  98. package/dist/chunk-FWBCQQYZ.mjs +0 -711
  99. package/dist/chunk-FWBCQQYZ.mjs.map +0 -1
  100. package/dist/index.d.mts +0 -1010
  101. package/dist/index.d.ts +0 -1010
  102. package/dist/index.js +0 -27294
  103. package/dist/index.js.map +0 -1
  104. package/dist/index.mjs +0 -959
  105. package/dist/index.mjs.map +0 -1
  106. package/dist/utils-B0pgWcps.d.mts +0 -303
  107. package/dist/utils-B0pgWcps.d.ts +0 -303
  108. package/dist/utils.d.mts +0 -2
  109. package/dist/utils.d.ts +0 -2
  110. package/dist/utils.js +0 -703
  111. package/dist/utils.js.map +0 -1
  112. package/dist/utils.mjs +0 -11
  113. package/dist/utils.mjs.map +0 -1
  114. package/example/.env.example +0 -12
  115. package/example/.eslintrc.json +0 -26
  116. package/example/LICENSE +0 -13
  117. package/example/README.md +0 -12
  118. package/example/app/(chat)/chat/[id]/page.tsx +0 -60
  119. package/example/app/(chat)/layout.tsx +0 -14
  120. package/example/app/(chat)/page.tsx +0 -27
  121. package/example/app/actions.ts +0 -156
  122. package/example/app/globals.css +0 -76
  123. package/example/app/guardrails/page.tsx +0 -26
  124. package/example/app/langchain/page.tsx +0 -27
  125. package/example/app/langchain-rag/page.tsx +0 -28
  126. package/example/app/late-update/page.tsx +0 -27
  127. package/example/app/layout.tsx +0 -64
  128. package/example/app/login/actions.ts +0 -71
  129. package/example/app/login/page.tsx +0 -18
  130. package/example/app/manual/page.tsx +0 -27
  131. package/example/app/new/page.tsx +0 -5
  132. package/example/app/opengraph-image.png +0 -0
  133. package/example/app/share/[id]/page.tsx +0 -58
  134. package/example/app/signup/actions.ts +0 -111
  135. package/example/app/signup/page.tsx +0 -18
  136. package/example/app/twitter-image.png +0 -0
  137. package/example/auth.config.ts +0 -42
  138. package/example/auth.ts +0 -45
  139. package/example/components/button-scroll-to-bottom.tsx +0 -36
  140. package/example/components/chat-history.tsx +0 -49
  141. package/example/components/chat-list.tsx +0 -52
  142. package/example/components/chat-message-actions.tsx +0 -40
  143. package/example/components/chat-message.tsx +0 -80
  144. package/example/components/chat-panel.tsx +0 -139
  145. package/example/components/chat-share-dialog.tsx +0 -95
  146. package/example/components/chat.tsx +0 -84
  147. package/example/components/clear-history.tsx +0 -75
  148. package/example/components/empty-screen.tsx +0 -38
  149. package/example/components/external-link.tsx +0 -29
  150. package/example/components/footer.tsx +0 -19
  151. package/example/components/header.tsx +0 -114
  152. package/example/components/login-button.tsx +0 -42
  153. package/example/components/login-form.tsx +0 -97
  154. package/example/components/markdown.tsx +0 -9
  155. package/example/components/prompt-form.tsx +0 -115
  156. package/example/components/providers.tsx +0 -17
  157. package/example/components/sidebar-actions.tsx +0 -125
  158. package/example/components/sidebar-desktop.tsx +0 -19
  159. package/example/components/sidebar-footer.tsx +0 -16
  160. package/example/components/sidebar-item.tsx +0 -124
  161. package/example/components/sidebar-items.tsx +0 -42
  162. package/example/components/sidebar-list.tsx +0 -38
  163. package/example/components/sidebar-mobile.tsx +0 -31
  164. package/example/components/sidebar-toggle.tsx +0 -24
  165. package/example/components/sidebar.tsx +0 -21
  166. package/example/components/signup-form.tsx +0 -95
  167. package/example/components/stocks/events-skeleton.tsx +0 -31
  168. package/example/components/stocks/events.tsx +0 -30
  169. package/example/components/stocks/index.tsx +0 -36
  170. package/example/components/stocks/message.tsx +0 -134
  171. package/example/components/stocks/spinner.tsx +0 -16
  172. package/example/components/stocks/stock-purchase.tsx +0 -146
  173. package/example/components/stocks/stock-skeleton.tsx +0 -22
  174. package/example/components/stocks/stock.tsx +0 -210
  175. package/example/components/stocks/stocks-skeleton.tsx +0 -9
  176. package/example/components/stocks/stocks.tsx +0 -67
  177. package/example/components/tailwind-indicator.tsx +0 -14
  178. package/example/components/theme-toggle.tsx +0 -31
  179. package/example/components/ui/alert-dialog.tsx +0 -141
  180. package/example/components/ui/badge.tsx +0 -36
  181. package/example/components/ui/button.tsx +0 -57
  182. package/example/components/ui/codeblock.tsx +0 -148
  183. package/example/components/ui/dialog.tsx +0 -122
  184. package/example/components/ui/dropdown-menu.tsx +0 -205
  185. package/example/components/ui/icons.tsx +0 -507
  186. package/example/components/ui/input.tsx +0 -25
  187. package/example/components/ui/label.tsx +0 -26
  188. package/example/components/ui/select.tsx +0 -164
  189. package/example/components/ui/separator.tsx +0 -31
  190. package/example/components/ui/sheet.tsx +0 -140
  191. package/example/components/ui/sonner.tsx +0 -31
  192. package/example/components/ui/switch.tsx +0 -29
  193. package/example/components/ui/textarea.tsx +0 -24
  194. package/example/components/ui/tooltip.tsx +0 -30
  195. package/example/components/user-menu.tsx +0 -53
  196. package/example/components.json +0 -17
  197. package/example/instrumentation.ts +0 -11
  198. package/example/lib/chat/guardrails.tsx +0 -181
  199. package/example/lib/chat/langchain-rag.tsx +0 -191
  200. package/example/lib/chat/langchain.tsx +0 -112
  201. package/example/lib/chat/late-update.tsx +0 -208
  202. package/example/lib/chat/manual.tsx +0 -605
  203. package/example/lib/chat/vercel-ai.tsx +0 -576
  204. package/example/lib/hooks/use-copy-to-clipboard.tsx +0 -33
  205. package/example/lib/hooks/use-enter-submit.tsx +0 -23
  206. package/example/lib/hooks/use-local-storage.ts +0 -24
  207. package/example/lib/hooks/use-scroll-anchor.tsx +0 -86
  208. package/example/lib/hooks/use-sidebar.tsx +0 -60
  209. package/example/lib/hooks/use-streamable-text.ts +0 -25
  210. package/example/lib/types.ts +0 -41
  211. package/example/lib/utils.ts +0 -89
  212. package/example/middleware.ts +0 -8
  213. package/example/next-env.d.ts +0 -5
  214. package/example/next.config.js +0 -16
  215. package/example/package-lock.json +0 -9990
  216. package/example/package.json +0 -84
  217. package/example/pnpm-lock.yaml +0 -5712
  218. package/example/postcss.config.js +0 -6
  219. package/example/prettier.config.cjs +0 -34
  220. package/example/public/apple-touch-icon.png +0 -0
  221. package/example/public/favicon-16x16.png +0 -0
  222. package/example/public/favicon.ico +0 -0
  223. package/example/public/next.svg +0 -1
  224. package/example/public/thirteen.svg +0 -1
  225. package/example/public/vercel.svg +0 -1
  226. package/example/tailwind.config.ts +0 -81
  227. package/example/tsconfig.json +0 -35
  228. package/src/LangWatchExporter.ts +0 -91
  229. package/src/evaluations.ts +0 -219
  230. package/src/index.test.ts +0 -402
  231. package/src/langchain.ts +0 -557
  232. package/src/typeUtils.ts +0 -89
  233. package/src/types.ts +0 -79
  234. package/src/utils.ts +0 -205
  235. /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
+ });