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,71 @@
1
+ import { describe, it, expect, beforeEach, afterAll, vi } from "vitest";
2
+
3
+ describe("client config", () => {
4
+ let client: any;
5
+
6
+ beforeEach(async () => {
7
+ // Mock environment variables before importing the client module
8
+ vi.stubEnv('LANGWATCH_API_KEY', undefined);
9
+ vi.stubEnv('LANGWATCH_ENDPOINT', undefined);
10
+
11
+ // Reset modules to ensure fresh import after env stubbing
12
+ vi.resetModules();
13
+
14
+ // Dynamically import the client module
15
+ client = await import("../client.js");
16
+
17
+ // Reset config before each test
18
+ client.setConfig({
19
+ apiKey: "",
20
+ endpoint: void 0,
21
+ disableOpenTelemetryAutomaticSetup: false,
22
+ disableAutomaticInputCapture: false,
23
+ disableAutomaticOutputCapture: false,
24
+ });
25
+ });
26
+
27
+ afterAll(() => {
28
+ // Restore original environment
29
+ vi.unstubAllEnvs();
30
+ });
31
+
32
+ it("should use default values if nothing is set", () => {
33
+ expect(client.getApiKey()).toBe("");
34
+ expect(client.getEndpoint()).toBe("https://app.langwatch.ai");
35
+ expect(client.canAutomaticallyCaptureInput()).toBe(true);
36
+ expect(client.canAutomaticallyCaptureOutput()).toBe(true);
37
+ });
38
+
39
+ it("should use env vars if set", () => {
40
+ process.env.LANGWATCH_API_KEY = "env-key";
41
+ process.env.LANGWATCH_ENDPOINT = "https://env.endpoint";
42
+
43
+ client.setConfig({});
44
+
45
+ expect(client.getApiKey()).toBe("env-key");
46
+ expect(client.getEndpoint()).toBe("https://env.endpoint");
47
+ });
48
+
49
+ it("should update config with setConfig", () => {
50
+ client.setConfig({
51
+ apiKey: "test-key",
52
+ endpoint: "https://test.endpoint",
53
+ disableAutomaticInputCapture: true,
54
+ disableAutomaticOutputCapture: true,
55
+ });
56
+ expect(client.getApiKey()).toBe("test-key");
57
+ expect(client.getEndpoint()).toBe("https://test.endpoint");
58
+ expect(client.canAutomaticallyCaptureInput()).toBe(false);
59
+ expect(client.canAutomaticallyCaptureOutput()).toBe(false);
60
+ });
61
+
62
+ it("should not override values if undefined is passed", () => {
63
+ client.setConfig({
64
+ apiKey: "first-key",
65
+ endpoint: "https://first.endpoint",
66
+ });
67
+ client.setConfig({ apiKey: undefined, endpoint: undefined });
68
+ expect(client.getApiKey()).toBe("first-key");
69
+ expect(client.getEndpoint()).toBe("https://first.endpoint");
70
+ });
71
+ });
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { setupLangWatch as setupBrowser } from '../../client-browser';
3
+
4
+ // Mock window object for Node.js test environment
5
+ const mockWindow = {
6
+ addEventListener: vi.fn(),
7
+ removeEventListener: vi.fn(),
8
+ };
9
+ global.window = mockWindow as any;
10
+
11
+ describe('client-browser integration', () => {
12
+ beforeEach(() => {
13
+ vi.clearAllMocks();
14
+ mockWindow.addEventListener.mockClear();
15
+ });
16
+
17
+ afterEach(() => {
18
+ // Clean up global state by clearing the module cache
19
+ vi.resetModules();
20
+ });
21
+
22
+ it('should start and shut down the WebTracerProvider without error', async () => {
23
+ // First setup should complete without error
24
+ await expect(setupBrowser({
25
+ apiKey: 'integration-key',
26
+ endpoint: 'http://localhost:9999',
27
+ skipOpenTelemetrySetup: false,
28
+ })).resolves.not.toThrow();
29
+
30
+ // Second setup should also complete without error (triggers shutdown of previous)
31
+ await expect(setupBrowser({
32
+ apiKey: 'integration-key2',
33
+ endpoint: 'http://localhost:9999',
34
+ skipOpenTelemetrySetup: false,
35
+ })).resolves.not.toThrow();
36
+ });
37
+
38
+ it('should not set up WebTracerProvider if skipOpenTelemetrySetup is true', async () => {
39
+ // Should complete without error and without setting up WebTracerProvider
40
+ await expect(setupBrowser({
41
+ apiKey: 'integration-key',
42
+ endpoint: 'http://localhost:9999',
43
+ skipOpenTelemetrySetup: true,
44
+ })).resolves.not.toThrow();
45
+ });
46
+ });
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+
3
+ describe('client-node integration', () => {
4
+ let clientNode: any;
5
+
6
+ beforeEach(async () => {
7
+ vi.clearAllMocks();
8
+ vi.resetModules();
9
+ // Import fresh modules for each test to get clean state
10
+ clientNode = await import('../../client-node.js');
11
+ });
12
+
13
+ afterEach(() => {
14
+ // Clean up global state by clearing the module cache
15
+ vi.resetModules();
16
+ });
17
+
18
+ it('should start and shut down the NodeSDK without error', async () => {
19
+ // First setup should complete without error
20
+ await expect(clientNode.setupLangWatch({
21
+ apiKey: 'integration-key',
22
+ endpoint: 'http://localhost:9999',
23
+ disableOpenTelemetryAutomaticSetup: false,
24
+ })).resolves.not.toThrow();
25
+
26
+ // Reset module to clear setupCalled state for second call
27
+ vi.resetModules();
28
+ clientNode = await import('../../client-node.js');
29
+
30
+ // Second setup should also complete without error (triggers shutdown of previous)
31
+ await expect(clientNode.setupLangWatch({
32
+ apiKey: 'integration-key2',
33
+ endpoint: 'http://localhost:9999',
34
+ disableOpenTelemetryAutomaticSetup: false,
35
+ })).resolves.not.toThrow();
36
+ });
37
+
38
+ it('should not start NodeSDK if disableOpenTelemetryAutomaticSetup is true', async () => {
39
+ // Should complete without error and without setting up NodeSDK
40
+ await expect(clientNode.setupLangWatch({
41
+ apiKey: 'integration-key',
42
+ endpoint: 'http://localhost:9999',
43
+ disableOpenTelemetryAutomaticSetup: true,
44
+ })).resolves.not.toThrow();
45
+ });
46
+ });
@@ -0,0 +1,70 @@
1
+ import { setConfig, SetupOptions, getApiKey, getEndpoint } from "./client";
2
+ import { SpanProcessor, WebTracerProvider } from '@opentelemetry/sdk-trace-web';
3
+ import { ZoneContextManager } from '@opentelemetry/context-zone';
4
+ import { W3CTraceContextPropagator } from '@opentelemetry/core';
5
+ import { version } from "../package.json";
6
+ import { resourceFromAttributes } from "@opentelemetry/resources";
7
+ import * as intSemconv from "./observability/semconv";
8
+ import { FilterableBatchSpanProcessor } from "./observability/processors";
9
+ import { LangWatchExporter } from "./observability/exporters";
10
+ import { addSpanProcessorToExistingTracerProvider, isOtelInitialized, mergeResourceIntoExistingTracerProvider } from "./client-shared";
11
+
12
+ let managedSpanProcessors: SpanProcessor[] = [];
13
+ let provider: WebTracerProvider | null = null;
14
+ let browserSetupCalled: boolean = false;
15
+
16
+ export async function setupLangWatch(options: SetupOptions = {}) {
17
+ if (browserSetupCalled) {
18
+ throw new Error("LangWatch setup has already been called in this process. Setup can only be called once, if you need to modify OpenTelemetry setup then use the OpenTelemetry API directly.");
19
+ }
20
+
21
+ setConfig(options);
22
+
23
+ if (options.skipOpenTelemetrySetup) return;
24
+
25
+ const endpointURL = new URL("/api/otel/v1/traces", getEndpoint());
26
+ const langwatchSpanProcessor = new FilterableBatchSpanProcessor(
27
+ new LangWatchExporter(getApiKey(), endpointURL.toString()),
28
+ options.otelSpanProcessingExcludeRules ?? [],
29
+ );
30
+
31
+ const langwatchResource = resourceFromAttributes({
32
+ ...options.baseAttributes,
33
+ [intSemconv.ATTR_LANGWATCH_SDK_LANGUAGE]: "typescript-browser",
34
+ [intSemconv.ATTR_LANGWATCH_SDK_VERSION]: version,
35
+ [intSemconv.ATTR_LANGWATCH_SDK_NAME]: "langwatch-observability-sdk",
36
+ });
37
+
38
+ if (isOtelInitialized()) {
39
+ mergeResourceIntoExistingTracerProvider(langwatchResource);
40
+ addSpanProcessorToExistingTracerProvider(langwatchSpanProcessor);
41
+ for (const spanProcessor of options.otelSpanProcessors ?? []) {
42
+ addSpanProcessorToExistingTracerProvider(spanProcessor);
43
+ }
44
+
45
+ managedSpanProcessors = [langwatchSpanProcessor];
46
+ } else {
47
+ provider = new WebTracerProvider({
48
+ resource: resourceFromAttributes({
49
+ [intSemconv.ATTR_LANGWATCH_SDK_LANGUAGE]: "typescript-browser",
50
+ [intSemconv.ATTR_LANGWATCH_SDK_VERSION]: version,
51
+ [intSemconv.ATTR_LANGWATCH_SDK_NAME]: "langwatch-observability-sdk",
52
+ }),
53
+ spanProcessors: [langwatchSpanProcessor, ...(options.otelSpanProcessors ?? [])],
54
+ });
55
+
56
+ provider.register({
57
+ contextManager: new ZoneContextManager(),
58
+ propagator: new W3CTraceContextPropagator(),
59
+ });
60
+ }
61
+
62
+ // This is not guaranteed to be called, but it's a good nice to have.
63
+ window.addEventListener("beforeunload", async () => {
64
+ if (provider) {
65
+ await provider.shutdown();
66
+ } else {
67
+ await Promise.all(managedSpanProcessors.map(p => p.shutdown()));
68
+ }
69
+ });
70
+ }
@@ -0,0 +1,82 @@
1
+ import { SpanProcessor } from "@opentelemetry/sdk-trace-base";
2
+ import { getApiKey, getEndpoint, setConfig, SetupOptions } from "./client";
3
+ import { NodeSDK } from '@opentelemetry/sdk-node';
4
+ import { resourceFromAttributes } from "@opentelemetry/resources";
5
+ import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
6
+ import { W3CTraceContextPropagator } from "@opentelemetry/core";
7
+ import { version } from "../package.json";
8
+ import * as intSemconv from "./observability/semconv";
9
+ import { addSpanProcessorToExistingTracerProvider, isOtelInitialized, mergeResourceIntoExistingTracerProvider } from "./client-shared";
10
+ import { FilterableBatchSpanProcessor } from "./observability";
11
+ import { LangWatchExporter } from "./observability/exporters";
12
+
13
+ let managedSpanProcessors: SpanProcessor[] = [];
14
+ let nodeSetupCalled: boolean = false;
15
+ let sdk: NodeSDK | null = null;
16
+
17
+ export async function setupLangWatch(options: SetupOptions = {}) {
18
+ if (nodeSetupCalled) {
19
+ throw new Error("LangWatch setup has already been called in this process. Setup can only be called once, if you need to modify OpenTelemetry setup then use the OpenTelemetry API directly.");
20
+ }
21
+
22
+ setConfig(options);
23
+ nodeSetupCalled = true;
24
+
25
+ if (options.skipOpenTelemetrySetup) return;
26
+
27
+ const endpointURL = new URL("/api/otel/v1/traces", getEndpoint());
28
+ const langwatchSpanProcessor = new FilterableBatchSpanProcessor(
29
+ new LangWatchExporter(getApiKey(), endpointURL.toString()),
30
+ options.otelSpanProcessingExcludeRules ?? [],
31
+ );
32
+
33
+ const langwatchResource = resourceFromAttributes({
34
+ ...options.baseAttributes,
35
+ [intSemconv.ATTR_LANGWATCH_SDK_LANGUAGE]: "typescript-node",
36
+ [intSemconv.ATTR_LANGWATCH_SDK_VERSION]: version,
37
+ [intSemconv.ATTR_LANGWATCH_SDK_NAME]: "langwatch-observability-sdk",
38
+ });
39
+
40
+ if (isOtelInitialized()) {
41
+ mergeResourceIntoExistingTracerProvider(langwatchResource);
42
+ addSpanProcessorToExistingTracerProvider(langwatchSpanProcessor);
43
+ for (const spanProcessor of options.otelSpanProcessors ?? []) {
44
+ addSpanProcessorToExistingTracerProvider(spanProcessor);
45
+ }
46
+
47
+ managedSpanProcessors = [langwatchSpanProcessor];
48
+ } else {
49
+ sdk = new NodeSDK({
50
+ resource: langwatchResource,
51
+ spanProcessors: [langwatchSpanProcessor, ...(options.otelSpanProcessors ?? [])],
52
+ contextManager: new AsyncLocalStorageContextManager(),
53
+ textMapPropagator: new W3CTraceContextPropagator(),
54
+ });
55
+
56
+ sdk.start();
57
+ }
58
+
59
+ // If we detect interrupt, termination, or test beforeExit signals, then we attempt
60
+ // to shutdown.
61
+ // - If an SDK exists, then we just attempt to shutdown the SDK.
62
+ // - If no SDK exists, then we attempt to shutdown ONLY the SpanProcessors that are
63
+ // managed by this LangWatch SDK.
64
+ ["SIGINT", "SIGTERM", "beforeExit"].forEach((signal) => {
65
+ process.on(signal as any, async () => {
66
+ try {
67
+ if (sdk) {
68
+ await sdk.shutdown();
69
+ } else {
70
+ await Promise.all(managedSpanProcessors.map(p => p.shutdown()));
71
+ }
72
+ } catch (error) {
73
+ // eslint-disable-next-line no-console
74
+ console.error("Error shutting down OpenTelemetry SDK:", error);
75
+ }
76
+
77
+ if (signal !== "beforeExit") {
78
+ process.exit();
79
+ }
80
+ });
81
+ });
82
+ }
@@ -0,0 +1,72 @@
1
+ import {
2
+ trace,
3
+ ProxyTracerProvider,
4
+ } from "@opentelemetry/api";
5
+ import { Resource } from "@opentelemetry/resources";
6
+ import { SpanProcessor } from "@opentelemetry/sdk-trace-base";
7
+
8
+ /**
9
+ * Gets the actual tracer provider, handling the proxy delegate pattern.
10
+ *
11
+ * @returns The actual tracer provider or undefined if not available
12
+ */
13
+ function getActualTracerProvider(): any {
14
+ const potentiallyProxyTracerProvider = trace.getTracerProvider() as unknown;
15
+
16
+ // Attempt to get the delegate if it's a ProxyTracerProvider
17
+ const delegate = (potentiallyProxyTracerProvider as ProxyTracerProvider | undefined)?.getDelegate?.();
18
+
19
+ // Return the delegate if available, otherwise return the original provider
20
+ return delegate ?? (potentiallyProxyTracerProvider as any);
21
+ }
22
+
23
+ /**
24
+ * Checks if the OpenTelemetry SDK has been initialized anywhere in the process.
25
+ *
26
+ * @returns true if the OpenTelemetry SDK has been initialized, false otherwise.
27
+ */
28
+ export function isOtelInitialized() {
29
+ const provider = getActualTracerProvider();
30
+
31
+ // Check if the provider has the addSpanProcessor method, which indicates SDK initialization
32
+ return provider && typeof provider.addSpanProcessor === "function";
33
+ }
34
+
35
+ /**
36
+ * Merges a resource into the existing tracer provider.
37
+ *
38
+ * @param resource - The resource to merge into the existing tracer provider.
39
+ */
40
+ export function mergeResourceIntoExistingTracerProvider(resource: Resource) {
41
+ if (!isOtelInitialized()) {
42
+ throw new Error("OpenTelemetry SDK is not initialized, cannot merge resource into existing tracer provider.");
43
+ }
44
+
45
+ const provider = getActualTracerProvider();
46
+
47
+ if (!provider?.resource) {
48
+ throw new Error("OpenTelemetry SDK is not initialized, provider does not have a resource.");
49
+ }
50
+ if (typeof resource !== "object") {
51
+ throw new Error("OpenTelemetry SDK is not initialized, provider resource is not an object.");
52
+ }
53
+ if (typeof provider.resource.merge !== "function") {
54
+ throw new Error("OpenTelemetry SDK is not initialized, provider resource does not have a merge method.");
55
+ }
56
+
57
+ provider.resource = provider.resource.merge(resource);
58
+ }
59
+
60
+ export function addSpanProcessorToExistingTracerProvider(spanProcessor: SpanProcessor) {
61
+ if (!isOtelInitialized()) {
62
+ throw new Error("OpenTelemetry SDK is not initialized, cannot add span processor to existing tracer provider.");
63
+ }
64
+
65
+ const provider = getActualTracerProvider();
66
+
67
+ if (!provider?.addSpanProcessor) {
68
+ throw new Error("OpenTelemetry SDK is not initialized, provider does not have a addSpanProcessor method.");
69
+ }
70
+
71
+ provider.addSpanProcessor(spanProcessor);
72
+ }
package/src/client.ts ADDED
@@ -0,0 +1,119 @@
1
+ import { SpanProcessor } from "@opentelemetry/sdk-trace-base";
2
+ import { SpanProcessingExcludeRule } from "./observability";
3
+ import { Attributes } from "@opentelemetry/api";
4
+
5
+ export interface SetupOptions {
6
+ /**
7
+ * The API key to use for the LangWatch API.
8
+ */
9
+ apiKey?: string;
10
+
11
+ /**
12
+ * The endpoint to use for the LangWatch API.
13
+ */
14
+ endpoint?: string;
15
+
16
+ /**
17
+ * The span processors to use for the OpenTelemetry SDK.
18
+ *
19
+ * If provided, these will be added to the OpenTelemetry SDK after the LangWatch SDK has
20
+ * been initialized.
21
+ */
22
+ otelSpanProcessors?: SpanProcessor[];
23
+
24
+ /**
25
+ * The span processing exclude rules to use for the OpenTelemetry SDK.
26
+ *
27
+ * If provided, these will be added to the OpenTelemetry SDK after the LangWatch SDK has
28
+ * been initialized.
29
+ *
30
+ * If you are using the `otelSpanProcessors` option, then these will be ignored.
31
+ */
32
+ otelSpanProcessingExcludeRules?: SpanProcessingExcludeRule[];
33
+
34
+ /**
35
+ * Whether to skip the automatic setup of the OpenTelemetry SDK. If this is set, then
36
+ * the LangWatch SDK will not attempt to setup the OpenTelemetry SDK. You will need to
37
+ * setup the OpenTelemetry yourself, and ensure that a SpanProcessor is added to the
38
+ * OpenTelemetry SDK that will send traces to the LangWatch API.
39
+ */
40
+ skipOpenTelemetrySetup?: boolean;
41
+
42
+ /**
43
+ * Whether to disable the automatic capture of input.
44
+ */
45
+ disableAutomaticInputCapture?: boolean;
46
+
47
+ /**
48
+ * Whether to disable the automatic capture of output.
49
+ */
50
+ disableAutomaticOutputCapture?: boolean;
51
+
52
+ /**
53
+ * The base attributes to use for the OpenTelemetry SDK.
54
+ */
55
+ baseAttributes?: Attributes;
56
+ }
57
+
58
+ interface InternalConfig {
59
+ apiKey: string;
60
+ endpoint: string;
61
+ setupCalled: boolean;
62
+ skipOpenTelemetrySetup: boolean;
63
+ disableAutomaticInputCapture: boolean;
64
+ disableAutomaticOutputCapture: boolean;
65
+
66
+ baseAttributes: Attributes;
67
+ }
68
+
69
+ const config: InternalConfig = {
70
+ apiKey: process.env.LANGWATCH_API_KEY ?? "",
71
+ endpoint: process.env.LANGWATCH_ENDPOINT ?? "https://app.langwatch.ai",
72
+ setupCalled: false,
73
+ skipOpenTelemetrySetup: false,
74
+ disableAutomaticInputCapture: false,
75
+ disableAutomaticOutputCapture: false,
76
+ baseAttributes: {},
77
+ };
78
+
79
+ export function setConfig(options: SetupOptions) {
80
+ config.setupCalled = true;
81
+
82
+ config.apiKey = options.apiKey !== void 0
83
+ ? options.apiKey
84
+ : (process.env.LANGWATCH_API_KEY ?? config.apiKey);
85
+
86
+ config.endpoint = options.endpoint !== void 0
87
+ ? options.endpoint
88
+ : (process.env.LANGWATCH_ENDPOINT ?? config.endpoint);
89
+
90
+ if (config.apiKey === "") {
91
+ console.warn("[langwatch setup] No API key provided. Please set the LANGWATCH_API_KEY environment variable or pass it to the setup function. The SDK will perform no operations.");
92
+ }
93
+
94
+ config.skipOpenTelemetrySetup = options.skipOpenTelemetrySetup ?? config.skipOpenTelemetrySetup;
95
+ config.disableAutomaticInputCapture = options.disableAutomaticInputCapture ?? config.disableAutomaticInputCapture;
96
+ config.disableAutomaticOutputCapture = options.disableAutomaticOutputCapture ?? config.disableAutomaticOutputCapture;
97
+
98
+ config.baseAttributes = options.baseAttributes ?? config.baseAttributes;
99
+ }
100
+
101
+ export function getApiKey(): string {
102
+ return config.apiKey;
103
+ }
104
+
105
+ export function getEndpoint(): string {
106
+ return config.endpoint;
107
+ }
108
+
109
+ export function canAutomaticallyCaptureInput(): boolean {
110
+ return !config.disableAutomaticInputCapture;
111
+ }
112
+
113
+ export function canAutomaticallyCaptureOutput(): boolean {
114
+ return !config.disableAutomaticOutputCapture;
115
+ }
116
+
117
+ export function isSetupCalled(): boolean {
118
+ return config.setupCalled;
119
+ }
@@ -0,0 +1,112 @@
1
+ // --- Mock setup (must be at the top for Vitest hoisting) ---
2
+ const { mockStartActiveSpan } = vi.hoisted(() => ({
3
+ mockStartActiveSpan: vi.fn((name, fn) => fn({
4
+ setType: vi.fn(),
5
+ addEvent: vi.fn(),
6
+ setOutput: vi.fn(),
7
+ setAttributes: vi.fn(),
8
+ setMetrics: vi.fn(),
9
+ setStatus: vi.fn(),
10
+ recordException: vi.fn(),
11
+ end: vi.fn(),
12
+ })),
13
+ }));
14
+
15
+ vi.mock('../tracer', () => ({ tracer: { startActiveSpan: mockStartActiveSpan } }));
16
+ vi.mock('../../observability/semconv', () => ({
17
+ ATTR_LANGWATCH_EVALUATION_CUSTOM: 'custom_event',
18
+ }));
19
+
20
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
21
+ import { recordEvaluation } from '../record-evaluation';
22
+
23
+ const baseDetails: import('../record-evaluation').RecordedEvaluationDetails = {
24
+ evaluationId: 'eval1',
25
+ name: 'test',
26
+ type: 'custom',
27
+ isGuardrail: false,
28
+ status: 'processed',
29
+ passed: true,
30
+ score: 1,
31
+ label: 'label',
32
+ details: 'ok',
33
+ cost: { currency: 'USD', amount: 0.1 },
34
+ error: undefined,
35
+ timestamps: { startedAtUnixMs: 1, finishedAtUnixMs: 2 },
36
+ };
37
+
38
+ describe('recordEvaluation', () => {
39
+ beforeEach(() => {
40
+ vi.clearAllMocks();
41
+ });
42
+
43
+ it('records processed evaluation', () => {
44
+ const span = {
45
+ setType: vi.fn(),
46
+ addEvent: vi.fn(),
47
+ setOutput: vi.fn(),
48
+ setAttributes: vi.fn(),
49
+ setMetrics: vi.fn(),
50
+ setOutputEvaluation: vi.fn(),
51
+ recordException: vi.fn(),
52
+ end: vi.fn(),
53
+ };
54
+
55
+ mockStartActiveSpan.mockImplementationOnce((name, fn) => fn(span));
56
+ recordEvaluation({ ...baseDetails });
57
+
58
+ expect(mockStartActiveSpan).toHaveBeenCalledWith(
59
+ 'record evaluation',
60
+ expect.any(Function)
61
+ );
62
+ expect(span.setType).toHaveBeenCalledWith('evaluation');
63
+ expect(span.addEvent).toHaveBeenCalledWith('custom_event', expect.objectContaining({
64
+ json_encoded_event: expect.stringContaining('"name":"test"')
65
+ }));
66
+ expect(span.setOutput).toHaveBeenCalledWith(expect.objectContaining({
67
+ status: 'processed',
68
+ passed: true,
69
+ score: 1,
70
+ label: 'label',
71
+ details: 'ok',
72
+ cost: { currency: 'USD', amount: 0.1 },
73
+ }));
74
+ expect(span.end).toHaveBeenCalled();
75
+ });
76
+
77
+ it('records skipped evaluation', () => {
78
+ recordEvaluation({ ...baseDetails, status: 'skipped', details: 'skipped' });
79
+ expect(mockStartActiveSpan).toHaveBeenCalled();
80
+ });
81
+
82
+ it('records error evaluation', () => {
83
+ recordEvaluation({ ...baseDetails, status: 'error', error: new Error('fail'), details: 'fail' });
84
+ expect(mockStartActiveSpan).toHaveBeenCalled();
85
+ });
86
+
87
+ it('sets cost metric if cost is present', () => {
88
+ recordEvaluation({ ...baseDetails, cost: { currency: 'USD', amount: 42 } });
89
+ // No assertion needed, just ensure no error
90
+ });
91
+
92
+ it('sets attributes if provided', () => {
93
+ const attrs = { foo: 'bar' };
94
+ recordEvaluation({ ...baseDetails }, attrs);
95
+ // No assertion needed, just ensure no error
96
+ });
97
+
98
+ it('handles error in span', () => {
99
+ const errorSpan = {
100
+ setType: vi.fn(() => { throw new Error('fail in span'); }),
101
+ addEvent: vi.fn(),
102
+ setOutput: vi.fn(),
103
+ setAttributes: vi.fn(),
104
+ setMetrics: vi.fn(),
105
+ recordException: vi.fn(),
106
+ end: vi.fn(),
107
+ };
108
+ expect(() => {
109
+ mockStartActiveSpan.mock.calls[0]?.[1]?.(errorSpan);
110
+ }).not.toThrow();
111
+ });
112
+ });