Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@
# AWS_SECRET_ACCESS_KEY=your-secret-key

# Langfuse (eval observability)
LANGFUSE_SECRET_KEY="sk-lf-your-personal-secret-key"
LANGFUSE_PUBLIC_KEY="pk-lf-your-personal-public-key"
LANGFUSE_BASE_URL="https://cloud.langfuse.com"
QUALOPS_LANGFUSE_SECRET_KEY="sk-lf-your-personal-secret-key"
QUALOPS_LANGFUSE_PUBLIC_KEY="pk-lf-your-personal-public-key"
QUALOPS_LANGFUSE_BASE_URL="https://cloud.langfuse.com"
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,9 @@ jobs:
BASE_REF: ${{ github.base_ref }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
CLAUDE_CODE_EXECUTABLE: ${{ github.workspace }}/node_modules/@anthropic-ai/claude-agent-sdk-linux-x64/claude
LANGFUSE_PUBLIC_KEY: ${{ secrets.LANGFUSE_PUBLIC_KEY }}
LANGFUSE_SECRET_KEY: ${{ secrets.LANGFUSE_SECRET_KEY }}
LANGFUSE_BASE_URL: ${{ vars.LANGFUSE_BASE_URL }}
QUALOPS_LANGFUSE_PUBLIC_KEY: ${{ secrets.QUALOPS_LANGFUSE_PUBLIC_KEY }}
QUALOPS_LANGFUSE_SECRET_KEY: ${{ secrets.QUALOPS_LANGFUSE_SECRET_KEY }}
QUALOPS_LANGFUSE_BASE_URL: ${{ vars.QUALOPS_LANGFUSE_BASE_URL }}
run: |
node dist/cli.js all \
--base "origin/$BASE_REF" \
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- A/B testing for QualOps config changes: run N config files against the same dataset and compare quality (precision/recall/F1) and cost/latency — for testing per-stage model choices, budgets, prompts, or pipeline modes. New `--config=<repo-relative-path>` eval flag runs against an arbitrary config file (validated: inside repo, `.json`, exists; precedence over `--preset`), so real ship configs are A/B-tested with no copy to drift. `evals/src/run-ab.ts` (TypeScript) takes configs via repeatable `--config=<path>` (same flag name as the qualops CLI), runs the full dataset per arm by default (scope with `--dataset`/`--limit`/`--repeats`, matching a normal eval run), and calls `evals/src/compare-experiments.ts`, which reads run-logs from `evals/logs/` (by path or experiment-label prefix via repeatable `--eval-log=<X>`) and renders a metric × arm table — one column per arm, N arms supported — with a held-or-up vs. regressed verdict for the 2-arm case. Run-log file/entry types are shared from `run-log.ts` (`RunLogFile`, `ItemCompleteEntry`). npm aliases: `eval:ab`, `eval:ab:compare`. Documented under "A/B testing configurations" in `evals/README.md`.

### Changed
- Langfuse env vars renamed to `QUALOPS_LANGFUSE_SECRET_KEY`/`QUALOPS_LANGFUSE_PUBLIC_KEY`/`QUALOPS_LANGFUSE_BASE_URL` so orgs can set them once across repos.
- `BASH_TOOL_DESCRIPTION` constant removed; callers use `buildBashToolDescription(root)` directly so the description always reflects the actual workspace root rather than a hardcoded `/workspace/pr`.
- Updated all dependencies to their latest stable versions (OpenTelemetry, `openai`, `@openai/agents`, `@types/node`, ESLint + `typescript-eslint`, `jest`/`ts-jest`, `prettier`, `zod`; website: Astro 6→7 + Starlight). Dropped the redundant `@types/diff` stub (diff@9 ships its own types) and pinned transitive `uuid` via `overrides`. Build, lint, type-check, unit tests, and evals all pass.

Expand Down
4 changes: 2 additions & 2 deletions evals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ Evaluates QualOps review quality against annotated code samples using [Langfuse]

1. `.env` in the qualops root with:
- `ANTHROPIC_API_KEY` (required)
- `LANGFUSE_SECRET_KEY`, `LANGFUSE_PUBLIC_KEY` (required)
- `LANGFUSE_BASE_URL` (optional, defaults to `https://cloud.langfuse.com`)
- `QUALOPS_LANGFUSE_SECRET_KEY`, `QUALOPS_LANGFUSE_PUBLIC_KEY` (required)
- `QUALOPS_LANGFUSE_BASE_URL` (optional, defaults to `https://cloud.langfuse.com`)
- `OPENAI_API_KEY` (optional, for dual-judge scoring)

2. Upload datasets to Langfuse:
Expand Down
10 changes: 6 additions & 4 deletions evals/src/run-eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -544,11 +544,13 @@ async function runDataset(langfuse: LangfuseClient, datasetName: string, tracer:
}

function createLangfuseClient(): { langfuse: LangfuseClient; host: string } {
const secretKey = process.env.LANGFUSE_SECRET_KEY;
const publicKey = process.env.LANGFUSE_PUBLIC_KEY;
const host = process.env.LANGFUSE_BASE_URL || 'https://cloud.langfuse.com';
const secretKey = process.env.QUALOPS_LANGFUSE_SECRET_KEY;
const publicKey = process.env.QUALOPS_LANGFUSE_PUBLIC_KEY;
const host = process.env.QUALOPS_LANGFUSE_BASE_URL || 'https://cloud.langfuse.com';
if (!secretKey || !publicKey) {
console.error('Error: LANGFUSE_SECRET_KEY and LANGFUSE_PUBLIC_KEY must be set in .env');
console.error(
'Error: QUALOPS_LANGFUSE_SECRET_KEY and QUALOPS_LANGFUSE_PUBLIC_KEY must be set in .env'
);
process.exit(1);
}
const langfuse = new LangfuseClient({ secretKey, publicKey, baseUrl: host });
Expand Down
10 changes: 6 additions & 4 deletions evals/src/upload-datasets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,12 +265,14 @@ function resolveCrbRepos(repoArg: string): string[] {
}

async function main(): Promise<void> {
const langfuseSecretKey = process.env.LANGFUSE_SECRET_KEY;
const langfusePublicKey = process.env.LANGFUSE_PUBLIC_KEY;
const langfuseHost = process.env.LANGFUSE_BASE_URL || 'https://cloud.langfuse.com';
const langfuseSecretKey = process.env.QUALOPS_LANGFUSE_SECRET_KEY;
const langfusePublicKey = process.env.QUALOPS_LANGFUSE_PUBLIC_KEY;
const langfuseHost = process.env.QUALOPS_LANGFUSE_BASE_URL || 'https://cloud.langfuse.com';

if (!langfuseSecretKey || !langfusePublicKey) {
console.error('Error: LANGFUSE_SECRET_KEY and LANGFUSE_PUBLIC_KEY must be set in .env');
console.error(
'Error: QUALOPS_LANGFUSE_SECRET_KEY and QUALOPS_LANGFUSE_PUBLIC_KEY must be set in .env'
);
process.exit(1);
}

Expand Down
9 changes: 6 additions & 3 deletions src/observability/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ let _state: TracingState | null = null;
* Initialize OpenTelemetry tracing.
*
* Auto-detection:
* 1. LANGFUSE_SECRET_KEY + LANGFUSE_PUBLIC_KEY → LangfuseSpanProcessor
* 1. QUALOPS_LANGFUSE_SECRET_KEY + QUALOPS_LANGFUSE_PUBLIC_KEY → LangfuseSpanProcessor
* 2. OTEL_EXPORTER_OTLP_ENDPOINT → raw OTLPTraceExporter
* 3. Neither → no-op (OTel default)
*/
Expand All @@ -29,7 +29,10 @@ export async function setupTracing(): Promise<void> {
return;
}

const hasLangfuse = !!(process.env.LANGFUSE_SECRET_KEY && process.env.LANGFUSE_PUBLIC_KEY);
const secretKey = process.env.QUALOPS_LANGFUSE_SECRET_KEY;
const publicKey = process.env.QUALOPS_LANGFUSE_PUBLIC_KEY;
const baseUrl = process.env.QUALOPS_LANGFUSE_BASE_URL || undefined;
const hasLangfuse = !!(secretKey && publicKey);
const hasOtlp = !!process.env.OTEL_EXPORTER_OTLP_ENDPOINT;

if (!hasLangfuse && !hasOtlp) return;
Expand All @@ -38,7 +41,7 @@ export async function setupTracing(): Promise<void> {

if (hasLangfuse) {
const { LangfuseSpanProcessor } = await import('@langfuse/otel');
const processor = new LangfuseSpanProcessor();
const processor = new LangfuseSpanProcessor({ secretKey, publicKey, baseUrl });
const sdk = new NodeSDK({ spanProcessors: [processor] });
sdk.start();
_state = { sdk, processor };
Expand Down
39 changes: 39 additions & 0 deletions tests/unit/observability/tracing-langfuse-processor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { shutdownTracing } from '@/observability';

const constructorSpy = jest.fn();

jest.mock('@langfuse/otel', () => ({
LangfuseSpanProcessor: class {
constructor(params: unknown) {
constructorSpy(params);
}
async forceFlush() {}
async shutdown() {}
onStart() {}
onEnd() {}
},
}));

describe('setupTracing (LangfuseSpanProcessor construction)', () => {
afterEach(async () => {
await shutdownTracing();
delete process.env.QUALOPS_LANGFUSE_SECRET_KEY;
delete process.env.QUALOPS_LANGFUSE_PUBLIC_KEY;
delete process.env.QUALOPS_LANGFUSE_BASE_URL;
});

it('constructs LangfuseSpanProcessor with explicit options read from QUALOPS_LANGFUSE_* env vars', async () => {
process.env.QUALOPS_LANGFUSE_SECRET_KEY = 'sk-test';
process.env.QUALOPS_LANGFUSE_PUBLIC_KEY = 'pk-test';
process.env.QUALOPS_LANGFUSE_BASE_URL = 'http://localhost:3000';

const { setupTracing } = await import('@/observability');
await setupTracing();

expect(constructorSpy).toHaveBeenCalledWith({
secretKey: 'sk-test',
publicKey: 'pk-test',
baseUrl: 'http://localhost:3000',
});
});
});
24 changes: 12 additions & 12 deletions tests/unit/observability/tracing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { setupTracing, getTracer, shutdownTracing, forceFlushTracing } from '@/o
describe('tracing', () => {
afterEach(async () => {
await shutdownTracing();
delete process.env.LANGFUSE_SECRET_KEY;
delete process.env.LANGFUSE_PUBLIC_KEY;
delete process.env.LANGFUSE_BASE_URL;
delete process.env.QUALOPS_LANGFUSE_SECRET_KEY;
delete process.env.QUALOPS_LANGFUSE_PUBLIC_KEY;
delete process.env.QUALOPS_LANGFUSE_BASE_URL;
delete process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
});

Expand All @@ -21,9 +21,9 @@ describe('tracing', () => {
});

it('registers a real TracerProvider when Langfuse keys are set', async () => {
process.env.LANGFUSE_SECRET_KEY = 'sk-test';
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test';
process.env.LANGFUSE_BASE_URL = 'http://localhost:3000';
process.env.QUALOPS_LANGFUSE_SECRET_KEY = 'sk-test';
process.env.QUALOPS_LANGFUSE_PUBLIC_KEY = 'pk-test';
process.env.QUALOPS_LANGFUSE_BASE_URL = 'http://localhost:3000';

await setupTracing();

Expand All @@ -36,9 +36,9 @@ describe('tracing', () => {
});

it('is idempotent — calling twice does not register a second provider', async () => {
process.env.LANGFUSE_SECRET_KEY = 'sk-test';
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test';
process.env.LANGFUSE_BASE_URL = 'http://localhost:3000';
process.env.QUALOPS_LANGFUSE_SECRET_KEY = 'sk-test';
process.env.QUALOPS_LANGFUSE_PUBLIC_KEY = 'pk-test';
process.env.QUALOPS_LANGFUSE_BASE_URL = 'http://localhost:3000';

await setupTracing();
const providerAfterFirst = (trace.getTracerProvider() as any).getDelegate();
Expand All @@ -53,9 +53,9 @@ describe('tracing', () => {

describe('shutdownTracing', () => {
it('clears state so a subsequent call is a no-op', async () => {
process.env.LANGFUSE_SECRET_KEY = 'sk-test';
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test';
process.env.LANGFUSE_BASE_URL = 'http://localhost:3000';
process.env.QUALOPS_LANGFUSE_SECRET_KEY = 'sk-test';
process.env.QUALOPS_LANGFUSE_PUBLIC_KEY = 'pk-test';
process.env.QUALOPS_LANGFUSE_BASE_URL = 'http://localhost:3000';

await setupTracing();
await shutdownTracing();
Expand Down
Loading