Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions .github/workflows/javascript-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,18 +81,30 @@ jobs:
run: pnpm smoke:dist
working-directory: javascript

- name: Lint
# Every workspace package except the root. Also asserts, before linting,
# that dist/ exists and that no package can escape the gate by omitting a
# lint script — both failures used to be silent (#565).
- name: Lint (workspace packages)
run: pnpm lint:all
working-directory: javascript

# Lints the shipped library source (src/, excluding tests) with the root
# package config, which the workspace-recursive lint:all does not cover.
# Enforces no-non-null-assertion (#751) and the rest of the config on the
# library; extending the gate to tests/examples is tracked in #565.
# library.
- name: Lint (library)
run: pnpm lint:lib
working-directory: javascript

# Everything the root package owns that lint:all does not reach: src/
# INCLUDING tests, root-level *.ts, and scripts/. The pre-existing
# no-explicit-any debt in test files is recorded per-file in
# eslint-suppressions.json rather than waived by severity, so a NEW one
# fails here while the known 106 stay green (#565).
- name: Lint (root package)
run: pnpm lint:root
working-directory: javascript

- name: Type check
run: pnpm typecheck:all
working-directory: javascript
Expand Down
2 changes: 1 addition & 1 deletion javascript/demo-sliding-deadline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
*
* Run: node_modules/.bin/tsx demo-sliding-deadline.ts
*/
import { ElevenLabsAgentAdapter } from "./src/voice/adapters/elevenlabs.js";
import { Buffer } from "node:buffer";
import type { RawData } from "ws";
import { ElevenLabsAgentAdapter } from "./src/voice/adapters/elevenlabs.js";

// ── timing constants ──────────────────────────────────────────────────────────
const TIMEOUT_S = 0.5; // 500ms raw idle deadline
Expand Down
17 changes: 17 additions & 0 deletions javascript/eslint-suppressions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"src/agents/__tests__/red-team.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 102
}
},
"src/agents/judge/__tests__/judge-agent.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 3
}
},
"src/tracing/__tests__/setup.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 1
}
}
}
12 changes: 9 additions & 3 deletions javascript/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,14 @@ export default defineConfig([
languageOptions: { globals: globals.browser },
},
{
files: ["**/*.config.{js,mjs,cjs,ts}", "eslint.config.mjs"],
// Build tooling and the maintenance scripts run under Node, not a browser.
// Without this, `scripts/**` is linted with browser globals and reports
// no-undef on Buffer/process (#565).
files: [
"**/*.config.{js,mjs,cjs,ts}",
"eslint.config.mjs",
"scripts/**/*.{js,mjs,cjs,ts}",
],
languageOptions: { globals: globals.node },
},
tseslint.configs.recommended,
Expand All @@ -78,8 +85,7 @@ export default defineConfig([
{
// Forbid non-null assertions (`!`) in shipped library source. Tests and
// examples legitimately assert known-present fixtures, so the rule is
// scoped to non-test `src/` only; extending the lint gate to the rest of
// the package is tracked in #565.
// scoped to non-test `src/` only.
files: ["src/**/*.ts"],
ignores: ["src/**/*.test.ts", "src/**/__tests__/**"],
rules: {
Expand Down
1 change: 1 addition & 0 deletions javascript/examples/custom-observability/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"private": true,
"type": "module",
"scripts": {
"lint": "eslint .",
"test:no-auto-init": "tsx test-no-auto-init.ts",
"test:scenario-only": "tsx test-scenario-only.ts",
"test:custom-scopes": "tsx test-custom-scopes.ts",
Expand Down
22 changes: 22 additions & 0 deletions javascript/examples/custom-observability/scope-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { ReadableSpan } from "@opentelemetry/sdk-trace-base";

/** The two shapes OTel has used for the instrumentation scope, across SDK majors. */
type ScopedSpan = ReadableSpan & {
instrumentationScope?: { name?: string };
instrumentationLibrary?: { name?: string };
};

/**
* Returns the instrumentation scope name for a span, handling both
* OTel SDK v1 (instrumentationLibrary) and v2 (instrumentationScope).
*
* Shared rather than copied into each probe: this is a version-compatibility
* shim, and the copy nobody remembers to update is the one that breaks when
* OTel renames the field again.
*/
export function getScopeName(span: ReadableSpan): string {
const s = span as ScopedSpan;
return (
s.instrumentationScope?.name ?? s.instrumentationLibrary?.name ?? "unknown"
);
}
22 changes: 11 additions & 11 deletions javascript/examples/custom-observability/test-config-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
process.chdir(path.join(__dirname, "with-config-file"));
console.log(`Working directory: ${process.cwd()}`);

import {
run,
AgentRole,
user,
agent,
succeed,
type AgentInput,
} from "@langwatch/scenario";
import { trace } from "@opentelemetry/api";
import type { ReadableSpan } from "@opentelemetry/sdk-trace-base";
import { run, AgentRole, user, agent, succeed } from "@langwatch/scenario";

function getScopeName(span: ReadableSpan): string {
const s = span as any;
return (
s.instrumentationScope?.name ??
s.instrumentationLibrary?.name ??
"unknown"
);
}
import { getScopeName } from "./scope-name";


// --- Step 1: Create agents (no LLM needed) ---
const dummyUserAgent = {
Expand All @@ -40,7 +40,7 @@ const dummyUserAgent = {

const echoAgent = {
role: AgentRole.AGENT as const,
call: async (input: any) => {
call: async (input: AgentInput) => {
const lastMessage = input.messages.at(-1);
const content =
typeof lastMessage?.content === "string"
Expand Down
31 changes: 10 additions & 21 deletions javascript/examples/custom-observability/test-custom-scopes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,6 @@
* Demonstrates the advanced use case where a user wants to include their own
* instrumented code (e.g., database calls) alongside scenario spans.
*/
import { trace } from "@opentelemetry/api";
import {
SimpleSpanProcessor,
InMemorySpanExporter,
} from "@opentelemetry/sdk-trace-base";
import type { ReadableSpan } from "@opentelemetry/sdk-trace-base";
import {
run,
AgentRole,
Expand All @@ -19,19 +13,13 @@ import {
setupScenarioTracing,
withCustomScopes,
} from "@langwatch/scenario";
import { trace } from "@opentelemetry/api";
import {
SimpleSpanProcessor,
InMemorySpanExporter,
} from "@opentelemetry/sdk-trace-base";
import { getScopeName } from "./scope-name";

/**
* Returns the instrumentation scope name for a span, handling both
* OTel SDK v1 (instrumentationLibrary) and v2 (instrumentationScope).
*/
function getScopeName(span: ReadableSpan): string {
const s = span as any;
return (
s.instrumentationScope?.name ??
s.instrumentationLibrary?.name ??
"unknown"
);
}

// --- Step 1: Set up span collection ---
const memoryExporter = new InMemorySpanExporter();
Expand All @@ -40,7 +28,7 @@ const collectorProcessor = new SimpleSpanProcessor(memoryExporter);
setupScenarioTracing({
instrumentations: [],
spanProcessors: [collectorProcessor],
langwatch: "disabled" as any,
langwatch: "disabled",
});

// --- Step 2: Create a "database" tracer under a custom scope ---
Expand All @@ -50,7 +38,7 @@ const httpTracer = trace.getTracer("http-server");
// Simulate a database-backed agent
const dbAgent = {
role: AgentRole.AGENT as const,
call: async (input: any) => {
call: async () => {
// Simulate a database query (tagged with custom scope)
return dbTracer.startActiveSpan(
"db.query SELECT users",
Expand Down Expand Up @@ -123,7 +111,8 @@ for (const [scope, scopeSpans] of byScope) {
// --- Step 6: Show what withCustomScopes would filter ---
const filters = withCustomScopes("my-app/database");
console.log("\n--- Filter config for LangWatchTraceExporter ---");
console.log('withCustomScopes("my-app/database") would include:');
console.log('withCustomScopes("my-app/database") returns:', filters);
console.log("which would include:");
console.log(
` @langwatch/scenario spans (${byScope.get("@langwatch/scenario")?.length ?? 0})`
);
Expand Down
5 changes: 3 additions & 2 deletions javascript/examples/custom-observability/test-no-auto-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import { trace } from "@opentelemetry/api";
const providerBefore = trace.getTracerProvider();
const providerNameBefore = providerBefore.constructor.name;

// Dynamically import scenario to test the side-effect
const scenario = await import("@langwatch/scenario");
// Dynamically import scenario to test the side-effect. The module namespace is
// deliberately discarded — the import itself is what this test exercises.
await import("@langwatch/scenario");

// Check the provider AFTER importing scenario
const providerAfter = trace.getTracerProvider();
Expand Down
30 changes: 9 additions & 21 deletions javascript/examples/custom-observability/test-scenario-only.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,22 @@
* This simulates the production use case: a server process that imports scenario
* and only wants scenario-scoped spans, not HTTP/middleware noise.
*/
import { trace } from "@opentelemetry/api";
import {
SimpleSpanProcessor,
InMemorySpanExporter,
} from "@opentelemetry/sdk-trace-base";
import type { ReadableSpan } from "@opentelemetry/sdk-trace-base";
import {
run,
AgentRole,
user,
agent,
succeed,
setupScenarioTracing,
scenarioOnly,
type AgentInput,
} from "@langwatch/scenario";
import { trace } from "@opentelemetry/api";
import {
SimpleSpanProcessor,
InMemorySpanExporter,
} from "@opentelemetry/sdk-trace-base";
import { getScopeName } from "./scope-name";

/**
* Returns the instrumentation scope name for a span, handling both
* OTel SDK v1 (instrumentationLibrary) and v2 (instrumentationScope).
*/
function getScopeName(span: ReadableSpan): string {
const s = span as any;
return (
s.instrumentationScope?.name ??
s.instrumentationLibrary?.name ??
"unknown"
);
}

// --- Step 1: Set up a custom InMemorySpanExporter to capture what gets collected ---
const memoryExporter = new InMemorySpanExporter();
Expand All @@ -41,7 +29,7 @@ const collectorProcessor = new SimpleSpanProcessor(memoryExporter);
setupScenarioTracing({
instrumentations: [], // disable auto-instrumentation
spanProcessors: [collectorProcessor],
langwatch: "disabled" as any, // don't send to LangWatch for this test
langwatch: "disabled", // don't send to LangWatch for this test
});

// --- Step 3: Simulate "server noise" -- create spans that should be filtered out ---
Expand All @@ -61,7 +49,7 @@ const dummyUserAgent = {

const echoAgent = {
role: AgentRole.AGENT as const,
call: async (input: any) => {
call: async (input: AgentInput) => {
const lastMessage = input.messages.at(-1);
const content =
typeof lastMessage?.content === "string"
Expand Down
2 changes: 1 addition & 1 deletion javascript/examples/openai-realtime-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"types": "index.ts",
"scripts": {
"typecheck": "tsc --noEmit",
"lint": "eslint agents/ index.ts",
"lint": "eslint . --ignore-pattern 'realtime-client/**'",
"format": "pnpm lint --fix"
},
"dependencies": {
Expand Down
3 changes: 2 additions & 1 deletion javascript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"typecheck": "tsc --noEmit",
"lint": "eslint .",
"lint:lib": "eslint 'src/**/*.ts' --ignore-pattern '**/*.test.ts' --ignore-pattern '**/__tests__/**'",
"lint:root": "eslint . --ignore-pattern 'examples/**'",
"format": "eslint . --fix",
"test": "vitest run",
"test:watch": "vitest",
Expand All @@ -29,7 +30,7 @@
"clean:all": "pnpm -r --parallel exec rm -rf dist *.tgz .cache",
"build:all": "pnpm run build",
"typecheck:all": "pnpm -r --parallel run typecheck",
"lint:all": "pnpm -r --parallel run lint",
"lint:all": "node scripts/check-lint-preconditions.mjs && pnpm -r --parallel run lint",
"format:all": "pnpm -r --parallel run format",
"test:all": "pnpm -r --parallel run test",
"vitest-examples": "pnpm -F vitest-examples",
Expand Down
Loading
Loading