langwatch 0.2.0 → 0.3.0-prerelease.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +16 -0
- package/LICENSE +7 -0
- package/README.md +268 -1
- package/copy-types.sh +19 -8
- package/examples/langchain/.env.example +2 -0
- package/examples/langchain/README.md +42 -0
- package/examples/langchain/package-lock.json +2930 -0
- package/examples/langchain/package.json +27 -0
- package/examples/langchain/src/cli-markdown.d.ts +137 -0
- package/examples/langchain/src/index.ts +109 -0
- package/examples/langchain/tsconfig.json +25 -0
- package/examples/langgraph/.env.example +2 -0
- package/examples/langgraph/README.md +42 -0
- package/examples/langgraph/package-lock.json +3031 -0
- package/examples/langgraph/package.json +28 -0
- package/examples/langgraph/src/cli-markdown.d.ts +137 -0
- package/examples/langgraph/src/index.ts +196 -0
- package/examples/langgraph/tsconfig.json +25 -0
- package/examples/mastra/.env.example +2 -0
- package/examples/mastra/README.md +57 -0
- package/examples/mastra/package-lock.json +5296 -0
- package/examples/mastra/package.json +32 -0
- package/examples/mastra/src/cli-markdown.d.ts +137 -0
- package/examples/mastra/src/index.ts +120 -0
- package/examples/mastra/src/mastra/agents/weather-agent.ts +30 -0
- package/examples/mastra/src/mastra/index.ts +21 -0
- package/examples/mastra/src/mastra/tools/weather-tool.ts +102 -0
- package/examples/mastra/tsconfig.json +25 -0
- package/examples/vercel-ai/.env.example +2 -0
- package/examples/vercel-ai/README.md +38 -0
- package/examples/vercel-ai/package-lock.json +2571 -0
- package/examples/vercel-ai/package.json +27 -0
- package/examples/vercel-ai/src/cli-markdown.d.ts +137 -0
- package/examples/vercel-ai/src/index.ts +110 -0
- package/examples/vercel-ai/src/instrumentation.ts +9 -0
- package/examples/vercel-ai/tsconfig.json +25 -0
- package/package.json +78 -34
- package/src/__tests__/client-browser.test.ts +92 -0
- package/src/__tests__/client-node.test.ts +76 -0
- package/src/__tests__/client.test.ts +71 -0
- package/src/__tests__/integration/client-browser.test.ts +46 -0
- package/src/__tests__/integration/client-node.test.ts +46 -0
- package/src/client-browser.ts +70 -0
- package/src/client-node.ts +82 -0
- package/src/client-shared.ts +72 -0
- package/src/client.ts +119 -0
- package/src/evaluation/__tests__/record-evaluation.test.ts +112 -0
- package/src/evaluation/__tests__/run-evaluation.test.ts +171 -0
- package/src/evaluation/index.ts +2 -0
- package/src/evaluation/record-evaluation.ts +101 -0
- package/src/evaluation/run-evaluation.ts +133 -0
- package/src/evaluation/tracer.ts +3 -0
- package/src/evaluation/types.ts +23 -0
- package/src/index.ts +10 -593
- package/src/internal/api/__tests__/errors.test.ts +98 -0
- package/src/internal/api/client.ts +30 -0
- package/src/internal/api/errors.ts +32 -0
- package/src/internal/generated/types/.gitkeep +0 -0
- package/src/observability/__tests__/integration/base.test.ts +74 -0
- package/src/observability/__tests__/integration/browser-setup-ordering.test.ts +60 -0
- package/src/observability/__tests__/integration/complex-nested-spans.test.ts +29 -0
- package/src/observability/__tests__/integration/error-handling.test.ts +24 -0
- package/src/observability/__tests__/integration/langwatch-disabled-otel.test.ts +24 -0
- package/src/observability/__tests__/integration/langwatch-first-then-vercel.test.ts +24 -0
- package/src/observability/__tests__/integration/multiple-setup-attempts.test.ts +27 -0
- package/src/observability/__tests__/integration/otel-ordering.test.ts +27 -0
- package/src/observability/__tests__/integration/vercel-configurations.test.ts +20 -0
- package/src/observability/__tests__/integration/vercel-first-then-langwatch.test.ts +27 -0
- package/src/observability/__tests__/span.test.ts +214 -0
- package/src/observability/__tests__/trace.test.ts +180 -0
- package/src/observability/exporters/index.ts +1 -0
- package/src/observability/exporters/langwatch-exporter.ts +53 -0
- package/src/observability/index.ts +4 -0
- package/src/observability/instrumentation/langchain/__tests__/integration/langchain-chatbot.test.ts +112 -0
- package/src/observability/instrumentation/langchain/__tests__/langchain.test.ts +284 -0
- package/src/observability/instrumentation/langchain/index.ts +624 -0
- package/src/observability/processors/__tests__/filterable-batch-span-exporter.test.ts +98 -0
- package/src/observability/processors/filterable-batch-span-processor.ts +99 -0
- package/src/observability/processors/index.ts +1 -0
- package/src/observability/semconv/attributes.ts +185 -0
- package/src/observability/semconv/events.ts +42 -0
- package/src/observability/semconv/index.ts +16 -0
- package/src/observability/semconv/values.ts +159 -0
- package/src/observability/span.ts +728 -0
- package/src/observability/trace.ts +301 -0
- package/src/prompt/__tests__/prompt.test.ts +139 -0
- package/src/prompt/get-prompt-version.ts +49 -0
- package/src/prompt/get-prompt.ts +44 -0
- package/src/prompt/index.ts +3 -0
- package/src/prompt/prompt.ts +133 -0
- package/src/prompt/service.ts +221 -0
- package/src/prompt/tracer.ts +3 -0
- package/src/prompt/types.ts +0 -0
- package/ts-to-zod.config.js +11 -0
- package/tsconfig.json +3 -9
- package/tsup.config.ts +11 -1
- package/vitest.config.ts +1 -0
- package/dist/chunk-LKD2K67J.mjs +0 -717
- package/dist/chunk-LKD2K67J.mjs.map +0 -1
- package/dist/index.d.mts +0 -1030
- package/dist/index.d.ts +0 -1030
- package/dist/index.js +0 -27310
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -963
- package/dist/index.mjs.map +0 -1
- package/dist/utils-Cv-rUjJ1.d.mts +0 -313
- package/dist/utils-Cv-rUjJ1.d.ts +0 -313
- package/dist/utils.d.mts +0 -2
- package/dist/utils.d.ts +0 -2
- package/dist/utils.js +0 -709
- package/dist/utils.js.map +0 -1
- package/dist/utils.mjs +0 -11
- package/dist/utils.mjs.map +0 -1
- package/example/.env.example +0 -12
- package/example/.eslintrc.json +0 -26
- package/example/LICENSE +0 -13
- package/example/README.md +0 -12
- package/example/app/(chat)/chat/[id]/page.tsx +0 -60
- package/example/app/(chat)/layout.tsx +0 -14
- package/example/app/(chat)/page.tsx +0 -27
- package/example/app/actions.ts +0 -156
- package/example/app/globals.css +0 -76
- package/example/app/guardrails/page.tsx +0 -26
- package/example/app/langchain/page.tsx +0 -27
- package/example/app/langchain-rag/page.tsx +0 -28
- package/example/app/late-update/page.tsx +0 -27
- package/example/app/layout.tsx +0 -64
- package/example/app/login/actions.ts +0 -71
- package/example/app/login/page.tsx +0 -18
- package/example/app/manual/page.tsx +0 -27
- package/example/app/new/page.tsx +0 -5
- package/example/app/opengraph-image.png +0 -0
- package/example/app/share/[id]/page.tsx +0 -58
- package/example/app/signup/actions.ts +0 -111
- package/example/app/signup/page.tsx +0 -18
- package/example/app/twitter-image.png +0 -0
- package/example/auth.config.ts +0 -42
- package/example/auth.ts +0 -45
- package/example/components/button-scroll-to-bottom.tsx +0 -36
- package/example/components/chat-history.tsx +0 -49
- package/example/components/chat-list.tsx +0 -52
- package/example/components/chat-message-actions.tsx +0 -40
- package/example/components/chat-message.tsx +0 -80
- package/example/components/chat-panel.tsx +0 -139
- package/example/components/chat-share-dialog.tsx +0 -95
- package/example/components/chat.tsx +0 -84
- package/example/components/clear-history.tsx +0 -75
- package/example/components/empty-screen.tsx +0 -38
- package/example/components/external-link.tsx +0 -29
- package/example/components/footer.tsx +0 -19
- package/example/components/header.tsx +0 -114
- package/example/components/login-button.tsx +0 -42
- package/example/components/login-form.tsx +0 -97
- package/example/components/markdown.tsx +0 -9
- package/example/components/prompt-form.tsx +0 -115
- package/example/components/providers.tsx +0 -17
- package/example/components/sidebar-actions.tsx +0 -125
- package/example/components/sidebar-desktop.tsx +0 -19
- package/example/components/sidebar-footer.tsx +0 -16
- package/example/components/sidebar-item.tsx +0 -124
- package/example/components/sidebar-items.tsx +0 -42
- package/example/components/sidebar-list.tsx +0 -38
- package/example/components/sidebar-mobile.tsx +0 -31
- package/example/components/sidebar-toggle.tsx +0 -24
- package/example/components/sidebar.tsx +0 -21
- package/example/components/signup-form.tsx +0 -95
- package/example/components/stocks/events-skeleton.tsx +0 -31
- package/example/components/stocks/events.tsx +0 -30
- package/example/components/stocks/index.tsx +0 -36
- package/example/components/stocks/message.tsx +0 -134
- package/example/components/stocks/spinner.tsx +0 -16
- package/example/components/stocks/stock-purchase.tsx +0 -146
- package/example/components/stocks/stock-skeleton.tsx +0 -22
- package/example/components/stocks/stock.tsx +0 -210
- package/example/components/stocks/stocks-skeleton.tsx +0 -9
- package/example/components/stocks/stocks.tsx +0 -67
- package/example/components/tailwind-indicator.tsx +0 -14
- package/example/components/theme-toggle.tsx +0 -31
- package/example/components/ui/alert-dialog.tsx +0 -141
- package/example/components/ui/badge.tsx +0 -36
- package/example/components/ui/button.tsx +0 -57
- package/example/components/ui/codeblock.tsx +0 -148
- package/example/components/ui/dialog.tsx +0 -122
- package/example/components/ui/dropdown-menu.tsx +0 -205
- package/example/components/ui/icons.tsx +0 -507
- package/example/components/ui/input.tsx +0 -25
- package/example/components/ui/label.tsx +0 -26
- package/example/components/ui/select.tsx +0 -164
- package/example/components/ui/separator.tsx +0 -31
- package/example/components/ui/sheet.tsx +0 -140
- package/example/components/ui/sonner.tsx +0 -31
- package/example/components/ui/switch.tsx +0 -29
- package/example/components/ui/textarea.tsx +0 -24
- package/example/components/ui/tooltip.tsx +0 -30
- package/example/components/user-menu.tsx +0 -53
- package/example/components.json +0 -17
- package/example/instrumentation.ts +0 -11
- package/example/lib/chat/guardrails.tsx +0 -181
- package/example/lib/chat/langchain-rag.tsx +0 -191
- package/example/lib/chat/langchain.tsx +0 -112
- package/example/lib/chat/late-update.tsx +0 -208
- package/example/lib/chat/manual.tsx +0 -605
- package/example/lib/chat/vercel-ai.tsx +0 -576
- package/example/lib/hooks/use-copy-to-clipboard.tsx +0 -33
- package/example/lib/hooks/use-enter-submit.tsx +0 -23
- package/example/lib/hooks/use-local-storage.ts +0 -24
- package/example/lib/hooks/use-scroll-anchor.tsx +0 -86
- package/example/lib/hooks/use-sidebar.tsx +0 -60
- package/example/lib/hooks/use-streamable-text.ts +0 -25
- package/example/lib/types.ts +0 -41
- package/example/lib/utils.ts +0 -89
- package/example/middleware.ts +0 -8
- package/example/next-env.d.ts +0 -5
- package/example/next.config.js +0 -16
- package/example/package-lock.json +0 -10917
- package/example/package.json +0 -84
- package/example/pnpm-lock.yaml +0 -5712
- package/example/postcss.config.js +0 -6
- package/example/prettier.config.cjs +0 -34
- package/example/public/apple-touch-icon.png +0 -0
- package/example/public/favicon-16x16.png +0 -0
- package/example/public/favicon.ico +0 -0
- package/example/public/next.svg +0 -1
- package/example/public/thirteen.svg +0 -1
- package/example/public/vercel.svg +0 -1
- package/example/tailwind.config.ts +0 -81
- package/example/tsconfig.json +0 -35
- package/src/LangWatchExporter.ts +0 -96
- package/src/evaluations.ts +0 -219
- package/src/index.test.ts +0 -402
- package/src/langchain.ts +0 -557
- package/src/typeUtils.ts +0 -89
- package/src/types.ts +0 -82
- package/src/utils.ts +0 -205
- /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
|
+
});
|