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,180 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { getLangWatchTracer } from '../trace';
3
+ import type { Tracer, Span, SpanOptions, Context } from '@opentelemetry/api';
4
+
5
+ // Mock createLangWatchSpan to just tag the span for test visibility
6
+ vi.mock('../span', () => ({
7
+ createLangWatchSpan: (span: Span) => ({
8
+ __isLangWatch: true,
9
+ ...span,
10
+ end: vi.fn(),
11
+ setStatus: vi.fn(),
12
+ recordException: vi.fn(),
13
+ }),
14
+ }));
15
+
16
+ // Helper to create a mock Tracer
17
+ function makeMockTracer() {
18
+ return {
19
+ startSpan: vi.fn((name, options, context) => ({ name, options, context })),
20
+ startActiveSpan: vi.fn((...args: any[]) => {
21
+ // OpenTelemetry's startActiveSpan calls the callback with the span
22
+ const fn = args[args.length - 1];
23
+ const span = { name: args[0], options: args[1], context: args[2] };
24
+ return fn(span);
25
+ }),
26
+ someOtherMethod: vi.fn(() => 'other'),
27
+ } as unknown as Tracer;
28
+ }
29
+
30
+ describe('getTracer', () => {
31
+ const origGetTracer = vi.hoisted(() => vi.fn());
32
+ let otelTrace: { getTracer: typeof origGetTracer };
33
+
34
+ beforeEach(() => {
35
+ otelTrace = require('@opentelemetry/api').trace;
36
+ otelTrace.getTracer = vi.fn(() => makeMockTracer());
37
+ });
38
+
39
+ it('returns a proxy with startSpan wrapping the span', () => {
40
+ const tracer = getLangWatchTracer('test');
41
+ const span = tracer.startSpan('my-span', { foo: 'bar' } as SpanOptions, {} as Context);
42
+ expect(span).toMatchObject({ __isLangWatch: true, name: 'my-span', options: { foo: 'bar' } });
43
+ });
44
+
45
+ it('returns a proxy with startActiveSpan wrapping the span in the callback', () => {
46
+ const tracer = getLangWatchTracer('test');
47
+ const result = tracer.startActiveSpan('active-span', (span: any) => {
48
+ expect(span).toMatchObject({ __isLangWatch: true, name: 'active-span' });
49
+ return 'done';
50
+ });
51
+ expect(result).toBe('done');
52
+ });
53
+
54
+ it('supports startActiveSpan with options and context overloads', () => {
55
+ const tracer = getLangWatchTracer('test');
56
+ let called = 0;
57
+ tracer.startActiveSpan('span1', { foo: 1 } as SpanOptions, (span: any) => {
58
+ expect(span).toMatchObject({ __isLangWatch: true, name: 'span1', options: { foo: 1 } });
59
+ called++;
60
+ });
61
+ tracer.startActiveSpan('span2', { foo: 2 } as SpanOptions, {} as Context, (span: any) => {
62
+ expect(span).toMatchObject({ __isLangWatch: true, name: 'span2', options: { foo: 2 }, context: {} });
63
+ called++;
64
+ });
65
+ expect(called).toBe(2);
66
+ });
67
+
68
+ it('supports startActiveSpan with a callback that returns a Promise', async () => {
69
+ const tracer = getLangWatchTracer('test');
70
+ const result = await tracer.startActiveSpan('promise-span', async (span: any) => {
71
+ expect(span).toMatchObject({ __isLangWatch: true, name: 'promise-span' });
72
+ await new Promise((resolve) => setTimeout(resolve, 10));
73
+ return 'async-done';
74
+ });
75
+ expect(result).toBe('async-done');
76
+ });
77
+
78
+ it('supports startActiveSpan with a callback that returns a thenable (Promise-like)', async () => {
79
+ const tracer = getLangWatchTracer('test');
80
+ const thenable = {
81
+ then: (resolve: (v: string) => void) => setTimeout(() => resolve('thenable-done'), 10),
82
+ };
83
+ const result = await tracer.startActiveSpan('thenable-span', (_span: any) => thenable);
84
+ expect(result).toBe('thenable-done');
85
+ });
86
+
87
+ it('forwards unknown methods to the underlying tracer', () => {
88
+ const tracer = getLangWatchTracer('test');
89
+ // @ts-expect-error
90
+ expect(tracer.someOtherMethod()).toBe('other');
91
+ });
92
+
93
+ it('throws if startActiveSpan is called without a function', () => {
94
+ const tracer = getLangWatchTracer('test');
95
+ // @ts-expect-error
96
+ expect(() => tracer.startActiveSpan('no-fn')).toThrow(/function as the last argument/);
97
+ });
98
+ });
99
+
100
+ describe('getTracer (withActiveSpan)', () => {
101
+ let tracer: ReturnType<typeof getLangWatchTracer>;
102
+ beforeEach(() => {
103
+ tracer = getLangWatchTracer('test');
104
+ });
105
+
106
+ it('returns a proxy with withActiveSpan wrapping the span', async () => {
107
+ const result = await tracer.withActiveSpan('my-span', (span: any) => {
108
+ expect(span).toMatchObject({ __isLangWatch: true, name: 'my-span' });
109
+ return 'done';
110
+ });
111
+ expect(result).toBe('done');
112
+ });
113
+
114
+ it('supports withActiveSpan with options and context overloads', async () => {
115
+ let called = 0;
116
+ await tracer.withActiveSpan('span1', { foo: 1 } as SpanOptions, (span: any) => {
117
+ expect(span).toMatchObject({ __isLangWatch: true, name: 'span1', options: { foo: 1 } });
118
+ called++;
119
+ });
120
+ await tracer.withActiveSpan('span2', { foo: 2 } as SpanOptions, {} as Context, (span: any) => {
121
+ expect(span).toMatchObject({ __isLangWatch: true, name: 'span2', options: { foo: 2 }, context: {} });
122
+ called++;
123
+ });
124
+ expect(called).toBe(2);
125
+ });
126
+
127
+ it('supports withActiveSpan with a callback that returns a Promise', async () => {
128
+ const result = await tracer.withActiveSpan('promise-span', async (span: any) => {
129
+ expect(span).toMatchObject({ __isLangWatch: true, name: 'promise-span' });
130
+ await new Promise((resolve) => setTimeout(resolve, 10));
131
+ return 'async-done';
132
+ });
133
+ expect(result).toBe('async-done');
134
+ });
135
+
136
+ it('supports withActiveSpan with a callback that returns a thenable (Promise-like)', async () => {
137
+ const thenable = {
138
+ then: (resolve: (v: string) => void) => setTimeout(() => resolve('thenable-done'), 10),
139
+ };
140
+ const result = await tracer.withActiveSpan('thenable-span', (_span: any) => thenable as any);
141
+ expect(result).toBe('thenable-done');
142
+ });
143
+
144
+ it('calls setStatus and recordException on error', async () => {
145
+ const error = new Error('fail!');
146
+ let spanRef: any = null;
147
+ const resultPromise = tracer.withActiveSpan('err-span', (span: any) => {
148
+ span.setStatus = vi.fn();
149
+ span.recordException = vi.fn();
150
+ spanRef = span;
151
+ throw error;
152
+ });
153
+ await expect(resultPromise).rejects.toThrow('fail!');
154
+ expect(spanRef.setStatus).toHaveBeenCalledWith({ code: expect.any(Number), message: 'fail!' });
155
+ expect(spanRef.recordException).toHaveBeenCalledWith(error);
156
+ });
157
+
158
+ it('throws if withActiveSpan is called without a function', async () => {
159
+ // @ts-expect-error
160
+ await expect(tracer.withActiveSpan('no-fn')).rejects.toThrow(/function as the last argument/);
161
+ });
162
+
163
+ it('ensures nested withActiveSpan calls propagate context (parent-child)', async () => {
164
+ const tracer = getLangWatchTracer('test');
165
+ let parentSpanRef: any = null;
166
+ let childSpanRef: any = null;
167
+ await tracer.withActiveSpan('parent-span', (parentSpan: any) => {
168
+ parentSpanRef = parentSpan;
169
+ return tracer.withActiveSpan('child-span', (childSpan: any) => {
170
+ childSpanRef = childSpan;
171
+ return 'nested';
172
+ });
173
+ });
174
+ // In the mock, context is just passed through, so we can check the parent/child linkage
175
+ expect(childSpanRef.context).toBe(parentSpanRef.context);
176
+ // Removed assertion on childSpanRef.options, as the mock does not reflect real OTel behavior
177
+ expect(childSpanRef.name).toBe('child-span');
178
+ expect(parentSpanRef.name).toBe('parent-span');
179
+ });
180
+ });
@@ -0,0 +1 @@
1
+ export * from "./langwatch-exporter";
@@ -0,0 +1,53 @@
1
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
2
+ import { version } from "../../../package.json";
3
+ import { getApiKey, getEndpoint } from "../../client";
4
+
5
+ /**
6
+ * LangWatchExporter extends the OpenTelemetry OTLP HTTP trace exporter
7
+ * to send trace data to LangWatch with proper authentication and metadata headers.
8
+ *
9
+ * This exporter automatically configures:
10
+ * - Authorization headers using the provided API key or environment variables/fallback
11
+ * - SDK version and language identification headers
12
+ * - Proper endpoint configuration for LangWatch ingestion using provided URL or environment variables/fallback
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * import { LangWatchExporter } from '@langwatch/observability';
17
+ *
18
+ * // Using environment variables/fallback configuration
19
+ * const exporter = new LangWatchExporter();
20
+ *
21
+ * // Using custom API key and endpoint
22
+ *
23
+ * // With environment variables/fallback configuration
24
+ * const exporter = new LangWatchExporter();
25
+ *
26
+ * // With custom API key and endpoint
27
+ * const exporter = new LangWatchExporter('api-key', 'https://custom.langwatch.com');
28
+ * ```
29
+ */
30
+ export class LangWatchExporter extends OTLPTraceExporter {
31
+ /**
32
+ * Creates a new LangWatchExporter instance.
33
+ *
34
+ * @param apiKey - Optional API key for LangWatch authentication. If not provided,
35
+ * will use environment variables or fallback configuration.
36
+ * @param endpointURL - Optional custom endpoint URL for LangWatch ingestion.
37
+ * If not provided, will use environment variables or fallback configuration.
38
+ */
39
+ constructor(apiKey?: string, endpointURL?: string) {
40
+ const setApiKey = apiKey ?? getApiKey();
41
+ const setEndpoint = endpointURL ?? getEndpoint();
42
+
43
+ super({
44
+ headers: {
45
+ "Authorization": `Bearer ${setApiKey}`,
46
+ "X-LangWatch-SDK-Version": version,
47
+ "X-LangWatch-SDK-Language": `typescript-${typeof process !== "undefined" ? "node" : "browser"}`,
48
+ "X-LangWatch-SDK-Name": "langwatch-observability-sdk",
49
+ },
50
+ url: setEndpoint,
51
+ });
52
+ }
53
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./span";
2
+ export * from "./trace";
3
+ export * from "./processors";
4
+ export * as semconv from "./semconv";
@@ -0,0 +1,112 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { ChatOpenAI } from "@langchain/openai";
3
+ import { LangWatchCallbackHandler } from "../../../langchain";
4
+ import { setupLangWatch } from "../../../../../client-node";
5
+ import { DynamicTool } from "@langchain/core/tools";
6
+ import { AgentExecutor, createToolCallingAgent, initializeAgentExecutorWithOptions } from "langchain/agents";
7
+ import { getLangWatchTracer } from "../../../../trace";
8
+ import { ChatPromptTemplate } from "@langchain/core/prompts";
9
+ import { beforeEach } from "node:test";
10
+
11
+ beforeEach(async () => {
12
+ await setupLangWatch();
13
+ });
14
+
15
+ describe("langchain chatbots", () => {
16
+ it("it should be able to do a simple question/response", async () => {
17
+ const tracer = getLangWatchTracer("langchain-chatbot.test");
18
+ await tracer.withActiveSpan("simple question/response", { root: true }, async () => {
19
+ const llm = new ChatOpenAI({
20
+ model: "gpt-4o-mini",
21
+ temperature: 0,
22
+ });
23
+
24
+ const result = await llm.invoke([
25
+ { role: "user", content: "Hi im Bob" },
26
+ ], { callbacks: [new LangWatchCallbackHandler()] });
27
+ expect(result.content).toContain("Bob");
28
+ });
29
+ });
30
+
31
+ it("it should be able to handle tool calls", async () => {
32
+ const tools = [
33
+ new DynamicTool({
34
+ name: "get_current_time",
35
+ description: "Returns the current time in ISO-8601 format.",
36
+ func: async () => new Date().toISOString(),
37
+ }),
38
+
39
+ new DynamicTool({
40
+ name: "multiply",
41
+ description: 'Multiply two numbers, provide input like "a,b".',
42
+ func: async (input: string) => {
43
+ const [a, b] = input.split(",").map(Number);
44
+
45
+ if (a === void 0 || b === void 0) {
46
+ throw new Error("Invalid input");
47
+ }
48
+
49
+ return String(a * b);
50
+ },
51
+ }),
52
+ ];
53
+
54
+ const tracer = getLangWatchTracer("langchain-chatbot.test");
55
+ await tracer.withActiveSpan(
56
+ "langchain tool call",
57
+ { root: true },
58
+ async () => {
59
+ const llm = new ChatOpenAI({
60
+ model: "gpt-4.1-mini",
61
+ temperature: 0,
62
+ });
63
+
64
+ const prompt = ChatPromptTemplate.fromMessages([
65
+ ["system", "You are a helpful assistant"],
66
+ ["placeholder", "{chat_history}"],
67
+ ["human", "{input}"],
68
+ ["placeholder", "{agent_scratchpad}"],
69
+ ]);
70
+
71
+ const agent = createToolCallingAgent({
72
+ llm,
73
+ tools,
74
+ prompt,
75
+ });
76
+ const agentExecutor = new AgentExecutor({
77
+ agent,
78
+ tools,
79
+ });
80
+
81
+ const tracingCallback = new LangWatchCallbackHandler();
82
+ const result = await agentExecutor.invoke({ input: "What time is it and what is 12 times 8?" }, { callbacks: [tracingCallback] });
83
+ expect(result.output).toContain("96");
84
+ },
85
+ );
86
+ });
87
+
88
+ it("should understand context grouping", async () => {
89
+ const tracer = getLangWatchTracer("langchain-chatbot.test");
90
+ await tracer.withActiveSpan(
91
+ "context grouping",
92
+ { root: true },
93
+ async () => {
94
+ const llm = new ChatOpenAI({
95
+ model: "gpt-4o-mini",
96
+ temperature: 0,
97
+ });
98
+
99
+ const tracingCallback = new LangWatchCallbackHandler();
100
+ const result1 = await llm.invoke([
101
+ { role: "user", content: "Hi im Alice" },
102
+ ], { callbacks: [tracingCallback] });
103
+ const result2 = await llm.invoke([
104
+ { role: "user", content: "Hi im Bob" },
105
+ ], { callbacks: [tracingCallback] });
106
+
107
+ expect(result1.content).toContain("Alice");
108
+ expect(result2.content).toContain("Bob");
109
+ },
110
+ );
111
+ });
112
+ });
@@ -0,0 +1,284 @@
1
+ // --- Mock setup (must be at the top for Vitest hoisting) ---
2
+ const { mockSpan, mockTracer } = vi.hoisted(() => {
3
+ const calls: any = { setType: [], setInput: [], setOutput: [], setAttributes: [], setRequestModel: [], setOutputString: [], setInputString: [], addEvent: [], recordException: [], setStatus: [], end: 0 };
4
+ const span = {
5
+ setType: vi.fn(function (this: any, ...args) { calls.setType.push(args); return this; }),
6
+ setInput: vi.fn(function (this: any, ...args) { calls.setInput.push(args); return this; }),
7
+ setOutput: vi.fn(function (this: any, ...args) { calls.setOutput.push(args); return this; }),
8
+ setAttributes: vi.fn(function (this: any, ...args) { calls.setAttributes.push(args); return this; }),
9
+ setRequestModel: vi.fn(function (this: any, ...args) { calls.setRequestModel.push(args); return this; }),
10
+ setOutputString: vi.fn(function (this: any, ...args) { calls.setOutputString.push(args); return this; }),
11
+ setInputString: vi.fn(function (this: any, ...args) { calls.setInputString.push(args); return this; }),
12
+ addEvent: vi.fn(function (this: any, ...args) { calls.addEvent.push(args); return this; }),
13
+ recordException: vi.fn((...args) => { calls.recordException.push(args); }),
14
+ setStatus: vi.fn(function (this: any, ...args) { calls.setStatus.push(args); return this; }),
15
+ setSelectedPrompt: vi.fn(function (this: any, ...args) { calls.setSelectedPrompt.push(args); return this; }),
16
+ end: vi.fn(() => { calls.end++; }),
17
+ setRAGContexts: vi.fn(function (this: any, ...args) { return this; }),
18
+ setRAGContext: vi.fn(function (this: any, ...args) { return this; }),
19
+ setResponseModel: vi.fn(function (this: any, ...args) { return this; }),
20
+ setMetrics: vi.fn(function (this: any, ...args) { return this; }),
21
+ setOutputEvaluation: vi.fn(function (this: any, ...args) { return this; }),
22
+ recordEvaluation: vi.fn(function (this: any, ...args) { return this; }),
23
+ addGenAISystemMessageEvent: vi.fn(function (this: any, ...args) { return this; }),
24
+ addGenAIUserMessageEvent: vi.fn(function (this: any, ...args) { return this; }),
25
+ addGenAIAssistantMessageEvent: vi.fn(function (this: any, ...args) { return this; }),
26
+ addGenAIToolMessageEvent: vi.fn(function (this: any, ...args) { return this; }),
27
+ addGenAIChoiceEvent: vi.fn(function (this: any, ...args) { return this; }),
28
+ spanContext: vi.fn(() => ({ traceId: 'trace', spanId: 'span', traceFlags: 1 })),
29
+ setAttribute: vi.fn(function (this: any, ...args) { return this; }),
30
+ addLink: vi.fn(function (this: any, ...args) { return this; }),
31
+ addLinks: vi.fn(function (this: any, ...args) { return this; }),
32
+ updateName: vi.fn(function (this: any, ...args) { return this; }),
33
+ isRecording: vi.fn(),
34
+ calls,
35
+ };
36
+ const tracer = {
37
+ startSpan: vi.fn(() => span),
38
+ startActiveSpan: vi.fn(() => span),
39
+ withActiveSpan: vi.fn(async (...args: any[]) => {
40
+ // Find the function argument (should be the last argument)
41
+ const fnIndex = args.findIndex((arg) => typeof arg === "function");
42
+ if (fnIndex === -1) {
43
+ throw new Error("withActiveSpan requires a function as the last argument");
44
+ }
45
+ const userFn = args[fnIndex] as (span: any) => any;
46
+ return await userFn(span);
47
+ }) as any,
48
+ };
49
+ return { mockSpan: span, mockTracer: tracer };
50
+ });
51
+
52
+ vi.mock('../../../trace', () => ({
53
+ getLangWatchTracer: vi.fn(() => mockTracer),
54
+ }));
55
+
56
+ vi.mock('@opentelemetry/api', async () => {
57
+ const actual = await vi.importActual<any>('@opentelemetry/api');
58
+ return {
59
+ ...actual,
60
+ context: {
61
+ ...actual.context,
62
+ active: vi.fn(() => ({})),
63
+ },
64
+ trace: {
65
+ ...actual.trace,
66
+ setSpan: vi.fn((_ctx, span) => span),
67
+ },
68
+ SpanStatusCode: { ERROR: 'ERROR' },
69
+ };
70
+ });
71
+
72
+ vi.mock('../../../client', () => ({
73
+ canAutomaticallyCaptureInput: () => true,
74
+ canAutomaticallyCaptureOutput: () => true,
75
+ }));
76
+
77
+ // --- Imports (must be after mocks for Vitest hoisting) ---
78
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
79
+ import { LangWatchCallbackHandler, convertFromLangChainMessages } from '..';
80
+
81
+ // --- Tests ---
82
+ describe('LangWatchCallbackHandler', () => {
83
+ let handler: LangWatchCallbackHandler;
84
+
85
+ beforeEach(() => {
86
+ handler = new LangWatchCallbackHandler();
87
+ handler.tracer = mockTracer;
88
+ vi.clearAllMocks();
89
+ });
90
+
91
+ afterEach(() => {
92
+ vi.clearAllMocks();
93
+ });
94
+
95
+ it('can be constructed', () => {
96
+ expect(handler).toBeInstanceOf(LangWatchCallbackHandler);
97
+ });
98
+
99
+ it('handleLLMStart creates a span and sets input/attributes', async () => {
100
+ const serializedMock = { lc: 1, type: 'constructor', id: ['llm', 'test'], kwargs: {} } as import('@langchain/core/load/serializable').SerializedConstructor;
101
+ await handler.handleLLMStart(
102
+ serializedMock,
103
+ ['prompt1', 'prompt2'],
104
+ 'run1',
105
+ undefined,
106
+ { temperature: 0.7 },
107
+ ['tag1'],
108
+ { ls_model_name: 'gpt-4', foo: 'bar' },
109
+ 'llmName',
110
+ );
111
+ expect(mockTracer.startSpan).toHaveBeenCalledWith('llmName', {}, expect.anything());
112
+ expect(mockSpan.setType).toHaveBeenCalledWith('llm');
113
+ expect(mockSpan.setInput).toHaveBeenCalledWith(['prompt1', 'prompt2']);
114
+ expect(mockSpan.setRequestModel).toHaveBeenCalledWith('gpt-4');
115
+ expect(mockSpan.setAttributes).toHaveBeenCalled();
116
+ expect(handler.spans['run1']).toBe(mockSpan);
117
+ });
118
+
119
+ it('handleLLMEnd sets output and ends span', async () => {
120
+ handler.spans['run2'] = mockSpan;
121
+ await handler.handleLLMEnd(
122
+ { generations: [[{ text: 'output1' }], [{ text: 'output2' }]] },
123
+ 'run2',
124
+ );
125
+ expect(mockSpan.setOutput).toHaveBeenCalled();
126
+ expect(mockSpan.end).toHaveBeenCalled();
127
+ expect(handler.spans['run2']).toBeUndefined();
128
+ });
129
+
130
+ it('handleLLMError records exception, sets error status, and ends span', async () => {
131
+ handler.spans['run3'] = mockSpan;
132
+ const error = new Error('fail');
133
+ await handler.handleLLMError(error, 'run3');
134
+ expect(mockSpan.recordException).toHaveBeenCalledWith(error);
135
+ expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: 'ERROR', message: 'fail' });
136
+ expect(mockSpan.end).toHaveBeenCalled();
137
+ expect(handler.spans['run3']).toBeUndefined();
138
+ });
139
+
140
+ it('handleChainStart creates a span and sets input', async () => {
141
+ const serializedMock = { lc: 1, type: 'constructor', id: ['chain', 'test'], kwargs: {} } as import('@langchain/core/load/serializable').SerializedConstructor;
142
+ const inputs = { foo: 'bar' };
143
+ await handler.handleChainStart(
144
+ serializedMock,
145
+ inputs,
146
+ 'chainRun1',
147
+ undefined,
148
+ undefined,
149
+ undefined,
150
+ undefined,
151
+ 'chainName',
152
+ );
153
+ expect(mockTracer.startSpan).toHaveBeenCalledWith('chainName', {}, expect.anything());
154
+ expect(mockSpan.setType).toHaveBeenCalledWith('chain');
155
+ expect(mockSpan.setInput).toHaveBeenCalledWith(inputs);
156
+ expect(handler.spans['chainRun1']).toBe(mockSpan);
157
+ });
158
+
159
+ it('handleChainEnd sets output and ends span', async () => {
160
+ handler.spans['chainRun2'] = mockSpan;
161
+ const output = { result: 'done' };
162
+ await handler.handleChainEnd(output, 'chainRun2');
163
+ expect(mockSpan.setOutput).toHaveBeenCalledWith(output);
164
+ expect(mockSpan.end).toHaveBeenCalled();
165
+ expect(handler.spans['chainRun2']).toBeUndefined();
166
+ });
167
+
168
+ it('handleChainError records exception, sets error status, and ends span', async () => {
169
+ handler.spans['chainRun3'] = mockSpan;
170
+ const error = new Error('chain fail');
171
+ await handler.handleChainError(error, 'chainRun3');
172
+ expect(mockSpan.recordException).toHaveBeenCalledWith(error);
173
+ expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: 'ERROR', message: 'chain fail' });
174
+ expect(mockSpan.end).toHaveBeenCalled();
175
+ expect(handler.spans['chainRun3']).toBeUndefined();
176
+ });
177
+
178
+ it('handleToolStart creates a span and sets input string', async () => {
179
+ const serializedMock = { lc: 1, type: 'constructor', id: ['tool', 'test'], kwargs: {} } as import('@langchain/core/load/serializable').SerializedConstructor;
180
+ await handler.handleToolStart(
181
+ serializedMock,
182
+ 'tool input',
183
+ 'toolRun1',
184
+ undefined,
185
+ ['tag1'],
186
+ { meta: 'data' },
187
+ 'toolName',
188
+ );
189
+ expect(mockTracer.startSpan).toHaveBeenCalledWith('toolName', {}, expect.anything());
190
+ expect(mockSpan.setType).toHaveBeenCalledWith('tool');
191
+ expect(mockSpan.setInputString).toHaveBeenCalledWith('tool input');
192
+ expect(mockSpan.setAttributes).toHaveBeenCalled();
193
+ expect(handler.spans['toolRun1']).toBe(mockSpan);
194
+ });
195
+
196
+ it('handleToolEnd sets output string and ends span', async () => {
197
+ handler.spans['toolRun2'] = mockSpan;
198
+ await handler.handleToolEnd('tool output', 'toolRun2');
199
+ expect(mockSpan.setOutputString).toHaveBeenCalledWith('tool output');
200
+ expect(mockSpan.end).toHaveBeenCalled();
201
+ expect(handler.spans['toolRun2']).toBeUndefined();
202
+ });
203
+
204
+ it('handleToolError records exception, sets error status, and ends span', async () => {
205
+ handler.spans['toolRun3'] = mockSpan;
206
+ const error = new Error('tool fail');
207
+ await handler.handleToolError(error, 'toolRun3');
208
+ expect(mockSpan.recordException).toHaveBeenCalledWith(error);
209
+ expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: 'ERROR', message: 'tool fail' });
210
+ expect(mockSpan.end).toHaveBeenCalled();
211
+ expect(handler.spans['toolRun3']).toBeUndefined();
212
+ });
213
+
214
+ it('handleRetrieverStart creates a span and sets input string', async () => {
215
+ const serializedMock = { lc: 1, type: 'constructor', id: ['retriever', 'test'], kwargs: {} } as import('@langchain/core/load/serializable').SerializedConstructor;
216
+ await handler.handleRetrieverStart(
217
+ serializedMock,
218
+ 'retriever query',
219
+ 'retrieverRun1',
220
+ undefined,
221
+ ['tag1'],
222
+ { meta: 'data' },
223
+ 'retrieverName',
224
+ );
225
+ expect(mockTracer.startSpan).toHaveBeenCalledWith('retrieverName', {}, expect.anything());
226
+ expect(mockSpan.setType).toHaveBeenCalledWith('rag');
227
+ expect(mockSpan.setInputString).toHaveBeenCalledWith('retriever query');
228
+ expect(mockSpan.setAttributes).toHaveBeenCalled();
229
+ expect(handler.spans['retrieverRun1']).toBe(mockSpan);
230
+ });
231
+
232
+ it('handleRetrieverEnd sets output, RAG contexts, and ends span', async () => {
233
+ handler.spans['retrieverRun2'] = mockSpan;
234
+ const docs = [
235
+ { metadata: { id: 'doc1', chunk_id: 'chunk1' }, pageContent: 'content1' },
236
+ { metadata: { id: 'doc2', chunk_id: 'chunk2' }, pageContent: 'content2' },
237
+ ];
238
+ await handler.handleRetrieverEnd(docs as any, 'retrieverRun2');
239
+ expect(mockSpan.setOutput).toHaveBeenCalledWith(docs);
240
+ expect(mockSpan.setRAGContexts).toHaveBeenCalledWith([
241
+ { document_id: 'doc1', chunk_id: 'chunk1', content: 'content1' },
242
+ { document_id: 'doc2', chunk_id: 'chunk2', content: 'content2' },
243
+ ]);
244
+ expect(mockSpan.end).toHaveBeenCalled();
245
+ expect(handler.spans['retrieverRun2']).toBeUndefined();
246
+ });
247
+
248
+ it('handleRetrieverError records exception, sets error status, and ends span', async () => {
249
+ handler.spans['retrieverRun3'] = mockSpan;
250
+ const error = new Error('retriever fail');
251
+ await handler.handleRetrieverError(error, 'retrieverRun3');
252
+ expect(mockSpan.recordException).toHaveBeenCalledWith(error);
253
+ expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: 'ERROR', message: 'retriever fail' });
254
+ expect(mockSpan.end).toHaveBeenCalled();
255
+ expect(handler.spans['retrieverRun3']).toBeUndefined();
256
+ });
257
+
258
+ it('handleAgentAction adds event and sets type', async () => {
259
+ handler.spans['agentRun1'] = mockSpan;
260
+ await handler.handleAgentAction({} as any, 'agentRun1');
261
+ expect(mockSpan.setType).toHaveBeenCalledWith('agent');
262
+ });
263
+
264
+ it('handleAgentEnd sets output, ends span, and cleans up', async () => {
265
+ handler.spans['agentRun2'] = mockSpan;
266
+ const action = { returnValues: { foo: 'bar' } };
267
+ await handler.handleAgentEnd(action as any, 'agentRun2');
268
+ expect(mockSpan.setOutput).toHaveBeenCalledWith(action.returnValues);
269
+ expect(mockSpan.end).toHaveBeenCalled();
270
+ expect(handler.spans['agentRun2']).toBeUndefined();
271
+ });
272
+
273
+ it('convertFromLangChainMessages converts messages to expected format', () => {
274
+ const messages = [
275
+ { content: 'hi', type: 'human', lc_serializable: false },
276
+ { content: 'hello', type: 'ai', lc_serializable: false },
277
+ ];
278
+ const result = convertFromLangChainMessages(messages as any);
279
+ expect(result[0].role).toBe('user');
280
+ expect(result[1].role).toBe('assistant');
281
+ expect(result[0].content).toBe('hi');
282
+ expect(result[1].content).toBe('hello');
283
+ });
284
+ });