Compare commits

...
9 Commits
Author SHA1 Message Date
Nimar 1bd069f24f chore: release v3.167.4 2026-04-10 19:33:39 +02:00
52dcb23953 chore(deps): override path-to-regexp to bump to non-vulnerable version (#12931)
'path-to-regexp' v0.1.13 was released 5 days ago to fix CVE-2026-4867
https://github.com/pillarjs/path-to-regexp/commit/7ccf02cee33402f06ed2125085992ee9cd3a7c45

This PR manually override `path-to-regexp` dependency coming from `dd-trace` to patch the CVE

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-04-10 19:33:05 +02:00
NimarandGitHub 2abaa0438e chore(ci): fix docker image upload (#13113)
* chore(ci): fix docker image upload

* simp
2026-04-10 17:31:11 +00:00
1e6d0a70a0 fix(worker): advance experiment backfill cursor when no items to process (#13107)
The backfill cursor was only advanced inside the chunk-processing loop,
which is never reached when the query returns zero dataset run items.
This caused last_run_delay_seconds to grow indefinitely in environments
with no recent experiment activity.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:41:29 +00:00
Nimar e91239e3ac chore: release v3.167.3 2026-04-10 17:35:44 +02:00
NimarandGitHub 418a2bf308 chore(ci): use blacksmith arm runners for docker image build (#13103)
* chore(ci): use blacksmith arm runners for docker image build

* stable rerun

* fix login
2026-04-10 15:34:13 +00:00
marliessophieandGitHub d08ce5bb71 style(experiment-compare): Add structured experiment color styles and visual accents for grid rows/columns (#13061)
* feat(web): improve experiment detail compare run styling

* fix(web): refine experiment compare accent markers

* fix(web): replace grid accent lines with marker bars

* style: improve

* refactor: streamline rendering logic in ExperimentItemsTable and enhance loading state presentation
2026-04-10 14:55:00 +00:00
baab82adae chore(ai): add skill to upgrade dependencies easily (#13094)
* chore(ai): add skill to upgrade dependencies easily

* fix(evals): prevent llm-as-a-judge queue stalls (#13037)

* clneaup

* bump transitive deps if possible

* up skill

* fix

* add timeout

---------

Co-authored-by: Hassieb Pakzad <68423100+hassiebp@users.noreply.github.com>
2026-04-10 14:54:45 +00:00
Valery MeleshkinandGitHub 932bd18df8 fix(codex): run clickhouse server as clickhouse user (#13101) 2026-04-10 16:01:38 +02:00
21 changed files with 1139 additions and 137 deletions
+3
View File
@@ -40,6 +40,9 @@ evaluating, and debugging AI applications.
[`skills/clickhouse-best-practices/SKILL.md`](skills/clickhouse-best-practices/SKILL.md)
- Monorepo/Turbo task graph changes:
[`skills/turborepo/SKILL.md`](skills/turborepo/SKILL.md)
- pnpm dependency upgrades, package-version bumps, or `minimumReleaseAgeExclude`
decisions in `pnpm-workspace.yaml`:
[`skills/pnpm-upgrade-package/SKILL.md`](skills/pnpm-upgrade-package/SKILL.md)
- User-visible frontend changes, Playwright review, or browser signoff:
[`skills/frontend-browser-review/SKILL.md`](skills/frontend-browser-review/SKILL.md)
- Web UI and frontend entry points:
+11
View File
@@ -81,6 +81,17 @@ Use for:
Open: [changelog-writing/SKILL.md](changelog-writing/SKILL.md)
### pnpm-upgrade-package
Use for:
- pnpm dependency bumps that need a specific target version
- interactive upgrades where the package name or version may be missing
- checking whether `pnpm-workspace.yaml` `minimumReleaseAgeExclude` must change
- comparing registry latest with the latest version installable under the
current release-age gate
Open: [pnpm-upgrade-package/SKILL.md](pnpm-upgrade-package/SKILL.md)
## Adding a New Shared Skill
1. Codex may create or refine shared skills under `.agents/skills/` when a
@@ -0,0 +1,72 @@
# PNPM Upgrade Package
Use this workflow when a user wants to upgrade a dependency in the Langfuse
pnpm workspace.
## Workflow
1. Collect missing inputs.
- Ask for the package if missing.
- Ask for the target version if missing.
- If the user says `latest`, resolve the real registry latest first.
2. Run the main helper once.
- Run
`node .agents/skills/pnpm-upgrade-package/scripts/check-release-age-window.mjs <package> <targetVersion>`.
- Treat this as the single source of truth for:
- direct workspace references
- root `pnpm.overrides` / `pnpm.patchedDependencies`
- latest registry version
- latest version installable under the current release-age rules
- existing matching `minimumReleaseAgeExclude` entries
- exact dependency companions from `dependencies` and `optionalDependencies`
- exact peer dependencies that are actually installed in the workspace
3. Handle the transitive-only case before editing anything.
- If the helper shows no direct workspace references, run `pnpm why -r <package>`.
- Identify the current top-level parent that pulls the package in.
- Check whether that parent's current dependency range already permits the
requested transitive version.
- If the current parent range already covers the requested version, prefer a
lock refresh / reinstall path before changing `package.json`.
- If the current parent range does not cover the requested version, upgrade
the direct parent dependency that pulls the package in.
- If a compatible transitive package still stays pinned after the normal
refresh path, you may suggest `pnpm dedupe` to the user as an optional
manual follow-up, but do not run it automatically and do not require it.
- Do not add the transitive package directly unless the user explicitly asks.
4. Ask before changing `minimumReleaseAgeExclude`.
- Prefer `package@version` entries.
- Only use bare `package` entries after explicit approval.
- Ask about exact companion packages only when the helper says they still
need a new exclusion.
- Treat range-based dependency or peer entries as manual review.
5. Bump at the narrowest useful scope.
- `pnpm -w up <package>@<version>` for root-only changes.
- `pnpm --filter <workspace> up <package>@<version>` for one workspace.
- `pnpm -r up <package>@<version>` only when every current reference should move.
- Do not hand-edit `pnpm-lock.yaml`.
6. Validate.
- Use the nearest package `AGENTS.md` plus the root verification matrix.
- Finish with `pnpm why -r <package>`.
- If companions moved too, run `pnpm why -r <companion-package>` for them as well.
## Quick Commands
- Run the single analysis pass:
`node .agents/skills/pnpm-upgrade-package/scripts/check-release-age-window.mjs <package> <targetVersion>`
- Transitive provenance check:
`pnpm why -r <package>`
- Inspect the current parent manifest on the registry:
`npm view <parent>@<installedVersion> dependencies peerDependencies optionalDependencies --json`
- Final graph verification:
`pnpm why -r <package>`
- Bump in the root workspace:
`pnpm -w up <package>@<version>`
- Bump in one workspace:
`pnpm --filter web up <package>@<version>`
- Bump everywhere that should move together:
`pnpm -r up <package>@<version>`
@@ -0,0 +1,42 @@
---
name: pnpm-upgrade-package
description: Use when upgrading a dependency in this pnpm workspace, including requests to bump a package to a specific version, compare the registry latest version with the latest version installable under the current minimum-release-age window, or decide whether minimumReleaseAgeExclude in pnpm-workspace.yaml must change. Ask the user for the package name or target version when either is missing.
---
# PNPM Upgrade Package
Use this skill for interactive dependency bumps in Langfuse.
## Read Order
- Start with [AGENTS.md](AGENTS.md) for the end-to-end workflow.
- Run the main helper once at the start of the upgrade:
`node .agents/skills/pnpm-upgrade-package/scripts/check-release-age-window.mjs <package> [targetVersion]`
## Apply This Skill
- Ask for the package name if the user did not provide one.
- Ask for the target version if the user did not provide one.
- Run the main helper once as the first analysis step and use that single
output for scope, exclusion decisions, and the final bump.
- If the target package is not directly declared anywhere, run
`pnpm why -r <package>` to find which direct dependency brings it in, then
inspect whether the current top-level parent already allows the requested
transitive version via its dependency range.
- If the current parent range already covers the requested transitive version,
prefer a lockfile refresh / reinstall path over bumping the parent manifest.
- If the current parent range does not cover the requested transitive version,
upgrade that parent dependency instead of adding the target package directly
unless the user explicitly wants that.
- If a compatible transitive package still stays pinned after the normal
refresh path, you may suggest `pnpm dedupe` to the user as an optional manual
follow-up, but do not run it automatically and do not require it.
- Resolve the registry latest version, but do not silently upgrade to latest
unless the user asked for latest.
- Compare the target version with the latest version installable under the
current `minimumReleaseAge` window.
- Ask before adding `minimumReleaseAgeExclude` entries for the target package,
exact dependency companions from `dependencies` or `optionalDependencies`, or
locally installed exact peer dependencies.
- Finish with `pnpm why -r <package>` to confirm that only the intended version
remains in the workspace.
@@ -0,0 +1,4 @@
interface:
display_name: "PNPM Upgrade Package"
short_description: "Interactive pnpm package bump workflow"
default_prompt: "Use $pnpm-upgrade-package to upgrade a package in this pnpm workspace, asking me for the package or version if I did not provide them."
@@ -0,0 +1,418 @@
#!/usr/bin/env node
import { join } from "node:path";
import {
entryCoversVersion,
findLocalPackageReferences,
formatWorkspaceReference,
getRootPnpmControls,
readWorkspaceConfig,
} from "./lib/workspace-utils.mjs";
const args = process.argv.slice(2);
const asJson = args.includes("--json");
const positional = args.filter((arg) => !arg.startsWith("--"));
const packageName = positional[0];
const requestedTargetVersion = positional[1] ?? null;
if (!packageName) {
console.error(
"Usage: node .agents/skills/pnpm-upgrade-package/scripts/check-release-age-window.mjs <package> [targetVersion] [--json]",
);
process.exit(1);
}
const repoRoot = process.cwd();
const workspaceConfig = readWorkspaceConfig(join(repoRoot, "pnpm-workspace.yaml"));
const minimumReleaseAgeMinutes = workspaceConfig.minimumReleaseAge ?? 0;
const thresholdMs = Date.now() - minimumReleaseAgeMinutes * 60 * 1000;
const REGISTRY_FETCH_TIMEOUT_MS = 30_000;
const registryCache = new Map();
const workspaceReferenceCache = new Map();
const getWorkspaceReferences = (name) => {
if (!workspaceReferenceCache.has(name)) {
workspaceReferenceCache.set(name, findLocalPackageReferences(repoRoot, name));
}
return workspaceReferenceCache.get(name);
};
function printSectionHeader(title) {
console.log("");
console.log(title);
}
function isPrerelease(version) {
return version.includes("-");
}
function isExactVersion(spec) {
return /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(spec.trim());
}
function getMatchingExcludeEntries(name, version) {
return workspaceConfig.minimumReleaseAgeExclude.filter((entry) =>
entryCoversVersion(entry, name, version),
);
}
async function fetchRegistryPackage(name) {
if (registryCache.has(name)) return registryCache.get(name);
const abortController = new AbortController();
const timeoutId = setTimeout(
() => abortController.abort(),
REGISTRY_FETCH_TIMEOUT_MS,
);
timeoutId.unref?.();
try {
const response = await fetch(
`https://registry.npmjs.org/${encodeURIComponent(name)}`,
{
headers: {
accept: "application/json",
"user-agent": "langfuse-pnpm-upgrade-package-skill",
},
signal: abortController.signal,
},
);
if (!response.ok) {
throw new Error(
`Failed to fetch ${name} from npm registry: ${response.status}`,
);
}
const metadata = await response.json();
registryCache.set(name, metadata);
return metadata;
} catch (error) {
if (error?.name === "AbortError") {
throw new Error(
`Timed out fetching ${name} from npm registry after ${REGISTRY_FETCH_TIMEOUT_MS}ms`,
{ cause: error },
);
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
function getInstallability(metadata, name, version) {
const publishedAt = metadata.time?.[version] ?? null;
const publishedAtMs = publishedAt ? Date.parse(publishedAt) : null;
const matchingExcludeEntries = getMatchingExcludeEntries(name, version);
const isYoungerThanMinimumReleaseAge =
publishedAtMs != null ? publishedAtMs > thresholdMs : null;
const isInstallableWithoutNewExclude =
isYoungerThanMinimumReleaseAge == null
? null
: !isYoungerThanMinimumReleaseAge || matchingExcludeEntries.length > 0;
return {
name,
version,
publishedAt,
isYoungerThanMinimumReleaseAge,
isInstallableWithoutNewExclude,
matchingExcludeEntries,
suggestedExclude:
isInstallableWithoutNewExclude === false ? `${name}@${version}` : null,
};
}
function selectLatestInstallableVersion(metadata, name) {
const times = metadata.time ?? {};
return (
Object.keys(metadata.versions ?? {})
.filter((version) => times[version] && !isPrerelease(version))
.sort((left, right) => Date.parse(times[right]) - Date.parse(times[left]))
.map((version) => getInstallability(metadata, name, version))
.find((candidate) => candidate.isInstallableWithoutNewExclude) ?? null
);
}
function collectManifestEntries(manifest, fields) {
const merged = new Map();
for (const field of fields) {
for (const [name, spec] of Object.entries(manifest[field] ?? {})) {
const key = `${name}:${spec}`;
const entry = merged.get(key);
if (entry) {
entry.fields.push(field);
continue;
}
merged.set(key, { name, spec, fields: [field] });
}
}
return [...merged.values()].sort((left, right) =>
left.name.localeCompare(right.name),
);
}
async function analyzeManifestEntries(entries, { includeWorkspace = false } = {}) {
const exact = [];
const range = [];
for (const entry of entries) {
const workspaceReferences = includeWorkspace
? getWorkspaceReferences(entry.name)
: null;
if (!isExactVersion(entry.spec)) {
range.push({
...entry,
...(includeWorkspace ? { workspaceReferences } : {}),
});
continue;
}
const metadata = await fetchRegistryPackage(entry.name);
const installability = getInstallability(metadata, entry.name, entry.spec);
exact.push({
...entry,
...installability,
...(includeWorkspace
? {
workspaceReferences,
isInstalledInWorkspace: workspaceReferences.length > 0,
}
: {}),
suggestedExclude:
includeWorkspace && workspaceReferences.length === 0
? null
: installability.suggestedExclude,
});
}
return { exact, range };
}
function printWorkspaceReferences(title, references) {
printSectionHeader(title);
if (references.length === 0) {
console.log("- none");
return;
}
for (const reference of references) {
console.log(`- ${formatWorkspaceReference(reference)}`);
}
}
function printRootPnpmControls(rootPnpm) {
printSectionHeader("Root pnpm controls:");
if (
rootPnpm.overrideMatches.length === 0 &&
rootPnpm.patchedDependencyMatches.length === 0
) {
console.log("- none");
return;
}
for (const match of rootPnpm.overrideMatches) {
console.log(`- override ${match.selector}: ${match.value}`);
}
for (const match of rootPnpm.patchedDependencyMatches) {
console.log(`- patched dependency ${match.selector}: ${match.value}`);
}
}
function printVersionEntries(title, entries, { includeWorkspace = false } = {}) {
printSectionHeader(title);
if (entries.length === 0) {
console.log("- none");
return;
}
for (const entry of entries) {
const status =
entry.isInstallableWithoutNewExclude == null
? "unknown"
: entry.isInstallableWithoutNewExclude
? "installable now"
: "needs exclude";
console.log(
`- ${entry.name}@${entry.version} (${status}; via ${entry.fields.join(", ")})`,
);
if (entry.publishedAt) {
console.log(` published at: ${entry.publishedAt}`);
}
if (includeWorkspace) {
console.log(
` installed in workspace: ${entry.isInstalledInWorkspace ? "yes" : "no"}`,
);
for (const reference of entry.workspaceReferences) {
console.log(` workspace reference: ${formatWorkspaceReference(reference)}`);
}
}
if (entry.matchingExcludeEntries.length > 0) {
console.log(
` matching exclude entries: ${entry.matchingExcludeEntries.join(", ")}`,
);
}
if (entry.suggestedExclude) {
console.log(` suggested exclude: ${entry.suggestedExclude}`);
}
}
}
function printRangeEntries(title, entries) {
printSectionHeader(title);
if (entries.length === 0) {
console.log("- none");
return;
}
for (const entry of entries) {
console.log(
`- ${entry.name}: ${entry.spec} (manual review; via ${entry.fields.join(", ")})`,
);
for (const reference of entry.workspaceReferences ?? []) {
console.log(` workspace reference: ${formatWorkspaceReference(reference)}`);
}
}
}
const packageMetadata = await fetchRegistryPackage(packageName);
const latestVersion = packageMetadata["dist-tags"]?.latest ?? null;
const targetVersion = requestedTargetVersion ?? latestVersion;
if (!targetVersion) {
console.error(`Could not resolve a target version for ${packageName}.`);
process.exit(1);
}
if (!packageMetadata.versions?.[targetVersion]) {
console.error(`Version ${targetVersion} was not found for ${packageName}.`);
process.exit(1);
}
const packageWorkspaceReferences = getWorkspaceReferences(packageName);
const rootPnpm = getRootPnpmControls(repoRoot, packageName);
const latestInstallableWithoutNewExclude = selectLatestInstallableVersion(
packageMetadata,
packageName,
);
const targetInstallability = getInstallability(
packageMetadata,
packageName,
targetVersion,
);
const targetManifest = packageMetadata.versions[targetVersion];
const dependencyCompanions = await analyzeManifestEntries(
collectManifestEntries(targetManifest, [
"dependencies",
"optionalDependencies",
]),
);
const peerDependencies = await analyzeManifestEntries(
collectManifestEntries(targetManifest, ["peerDependencies"]),
{ includeWorkspace: true },
);
const result = {
packageName,
targetVersion,
targetWasExplicitlyProvided: requestedTargetVersion != null,
packageWorkspaceReferences,
rootPnpm,
minimumReleaseAgeMinutes,
thresholdIso: new Date(thresholdMs).toISOString(),
latestRegistryVersion: latestVersion,
latestRegistryPublishedAt:
latestVersion != null ? packageMetadata.time?.[latestVersion] ?? null : null,
latestInstallableWithoutNewExclude,
targetPublishedAt: targetInstallability.publishedAt,
targetIsYoungerThanMinimumReleaseAge:
targetInstallability.isYoungerThanMinimumReleaseAge,
targetIsInstallableWithoutNewExclude:
targetInstallability.isInstallableWithoutNewExclude,
matchingPackageExcludeEntries: targetInstallability.matchingExcludeEntries,
suggestedPackageExclude: targetInstallability.suggestedExclude,
exactDependencyCompanions: dependencyCompanions.exact,
rangeDependencyCompanions: dependencyCompanions.range,
exactPeerDependencies: peerDependencies.exact,
rangePeerDependencies: peerDependencies.range,
};
if (asJson) {
console.log(JSON.stringify(result, null, 2));
process.exit(0);
}
console.log(`Package: ${packageName}`);
console.log(
`Target version: ${targetVersion}${
requestedTargetVersion
? ""
: " (resolved latest; still ask before bumping if version was omitted)"
}`,
);
printWorkspaceReferences(
"Target package workspace references:",
packageWorkspaceReferences,
);
printRootPnpmControls(rootPnpm);
printSectionHeader("Release-age window:");
console.log(`minimumReleaseAge: ${minimumReleaseAgeMinutes} minutes`);
console.log(`Threshold: ${result.thresholdIso}`);
console.log(`Latest registry version: ${result.latestRegistryVersion ?? "unknown"}`);
if (result.latestRegistryPublishedAt) {
console.log(`Latest registry published at: ${result.latestRegistryPublishedAt}`);
}
if (latestInstallableWithoutNewExclude) {
console.log(
`Latest installable without new exclude: ${latestInstallableWithoutNewExclude.version} (${latestInstallableWithoutNewExclude.publishedAt})`,
);
} else {
console.log("Latest installable without new exclude: none found");
}
console.log(`Target published at: ${result.targetPublishedAt ?? "unknown"}`);
console.log(
`Target installable without new exclude: ${
result.targetIsInstallableWithoutNewExclude == null
? "unknown"
: result.targetIsInstallableWithoutNewExclude
? "yes"
: "no"
}`,
);
if (result.matchingPackageExcludeEntries.length > 0) {
console.log("Matching package exclude entries:");
for (const entry of result.matchingPackageExcludeEntries) {
console.log(`- ${entry}`);
}
} else {
console.log("Matching package exclude entries: none");
}
if (result.suggestedPackageExclude) {
console.log(`Suggested package exclude: ${result.suggestedPackageExclude}`);
}
printVersionEntries(
"Exact dependency companions (dependencies + optionalDependencies):",
result.exactDependencyCompanions,
);
printRangeEntries(
"Range dependency companions (dependencies + optionalDependencies):",
result.rangeDependencyCompanions,
);
printVersionEntries("Exact peer dependencies:", result.exactPeerDependencies, {
includeWorkspace: true,
});
printRangeEntries("Range peer dependencies:", result.rangePeerDependencies);
@@ -0,0 +1,208 @@
import { existsSync, readFileSync, readdirSync } from "node:fs";
import { join, relative } from "node:path";
const packageFields = [
"dependencies",
"devDependencies",
"peerDependencies",
"optionalDependencies",
];
export function formatWorkspaceReference(reference) {
const label = reference.workspaceName
? `${reference.path} (${reference.workspaceName})`
: reference.path;
const specs = reference.matches
.map((match) => `${match.field}: ${match.spec}`)
.join(", ");
return `${label} -> ${specs}`;
}
export function readJson(path) {
return JSON.parse(readFileSync(path, "utf8"));
}
function stripInlineComment(line) {
let quote = null;
let escaped = false;
for (let index = 0; index < line.length; index += 1) {
const char = line[index];
if (escaped) {
escaped = false;
continue;
}
if (quote) {
if (char === "\\") {
escaped = true;
continue;
}
if (char === quote) {
quote = null;
}
continue;
}
if (char === "'" || char === '"') {
quote = char;
continue;
}
if (char === "#") {
return line.slice(0, index).trimEnd();
}
}
return line;
}
export function readWorkspaceConfig(path) {
const raw = readFileSync(path, "utf8");
const lines = raw.split(/\r?\n/);
let minimumReleaseAge = 0;
const minimumReleaseAgeExclude = [];
let inExcludeBlock = false;
for (const line of lines) {
const uncommented = stripInlineComment(line);
const trimmed = uncommented.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const ageMatch = trimmed.match(/^minimumReleaseAge:\s*(\d+)\s*$/);
if (ageMatch) {
minimumReleaseAge = Number(ageMatch[1]);
continue;
}
if (/^minimumReleaseAgeExclude:\s*$/.test(trimmed)) {
inExcludeBlock = true;
continue;
}
if (inExcludeBlock) {
const excludeMatch = uncommented.match(/^\s*-\s+(.+?)\s*$/);
if (excludeMatch) {
minimumReleaseAgeExclude.push(
excludeMatch[1].replace(/^['"]|['"]$/g, ""),
);
continue;
}
if (/^\S/.test(uncommented)) {
inExcludeBlock = false;
}
}
}
return { minimumReleaseAge, minimumReleaseAgeExclude };
}
export function collectPackageJsonPaths(repoRoot) {
const paths = [
"package.json",
"web/package.json",
"worker/package.json",
"ee/package.json",
];
const packagesRoot = join(repoRoot, "packages");
if (!existsSync(packagesRoot)) return paths;
const stack = [packagesRoot];
while (stack.length > 0) {
const current = stack.pop();
for (const entry of readdirSync(current, { withFileTypes: true })) {
if (
entry.name === "node_modules" ||
entry.name === "dist" ||
entry.name === ".git"
) {
continue;
}
const nextPath = join(current, entry.name);
if (entry.isDirectory()) {
stack.push(nextPath);
continue;
}
if (entry.isFile() && entry.name === "package.json") {
paths.push(relative(repoRoot, nextPath));
}
}
}
return [...new Set(paths)];
}
export function matchesPackageSelector(selector, wantedPackage) {
if (selector === wantedPackage) return true;
if (selector.startsWith(`${wantedPackage}@`)) return true;
if (selector.endsWith(`>${wantedPackage}`)) return true;
if (selector.includes(`>${wantedPackage}@`)) return true;
if (selector.endsWith("/*")) {
const prefix = selector.slice(0, -1);
return wantedPackage.startsWith(prefix);
}
return false;
}
export function entryCoversVersion(entry, wantedPackage, wantedVersion) {
if (entry === wantedPackage) return true;
if (entry.endsWith("/*")) {
const prefix = entry.slice(0, -1);
return wantedPackage.startsWith(prefix);
}
if (!entry.startsWith(`${wantedPackage}@`)) return false;
return entry
.slice(wantedPackage.length + 1)
.split("||")
.map((part) => part.trim())
.includes(wantedVersion);
}
export function findLocalPackageReferences(repoRoot, wantedPackage) {
const results = [];
for (const packageJsonPath of collectPackageJsonPaths(repoRoot)) {
const json = readJson(join(repoRoot, packageJsonPath));
const matches = [];
for (const field of packageFields) {
if (json[field]?.[wantedPackage]) {
matches.push({ field, spec: json[field][wantedPackage] });
}
}
if (matches.length > 0) {
results.push({
path: packageJsonPath,
workspaceName: json.name ?? null,
matches,
});
}
}
return results;
}
export function getRootPnpmControls(repoRoot, packageName) {
const rootPackageJson = readJson(join(repoRoot, "package.json"));
return {
overrideMatches: Object.entries(rootPackageJson.pnpm?.overrides ?? {})
.filter(([selector]) => matchesPackageSelector(selector, packageName))
.map(([selector, value]) => ({ selector, value })),
patchedDependencyMatches: Object.entries(
rootPackageJson.pnpm?.patchedDependencies ?? {},
)
.filter(([selector]) => matchesPackageSelector(selector, packageName))
.map(([selector, value]) => ({ selector, value })),
};
}
+193 -39
View File
@@ -631,25 +631,45 @@ jobs:
run: |
echo "Job results: ${{ steps.set-success-output.outputs.success }}"
push-docker-image:
build-docker-image-release:
needs: all-ci-passed
# if something inside all-ci-passed was skipped, but everything that ran passed, we still want to deploy
if: always() && needs.all-ci-passed.outputs.success == 'true' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
environment: "protected branches"
runs-on: blacksmith-4vcpu-ubuntu-2404
strategy:
fail-fast: false
matrix:
include:
- component: web
image_name: langfuse
dockerfile: ./web/Dockerfile
platform: linux/amd64
platform_tag: amd64
runner: blacksmith-4vcpu-ubuntu-2404
- component: web
image_name: langfuse
dockerfile: ./web/Dockerfile
platform: linux/arm64
platform_tag: arm64
runner: blacksmith-4vcpu-ubuntu-2404-arm
- component: worker
image_name: langfuse-worker
dockerfile: ./worker/Dockerfile
platform: linux/amd64
platform_tag: amd64
runner: blacksmith-4vcpu-ubuntu-2404
- component: worker
image_name: langfuse-worker
dockerfile: ./worker/Dockerfile
platform: linux/arm64
platform_tag: arm64
runner: blacksmith-4vcpu-ubuntu-2404-arm
runs-on: ${{ matrix.runner }}
permissions:
packages: write
contents: read
steps:
- uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3
with:
version: 10.33.0
- name: Setup node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24
cache-dependency-path: "pnpm-lock.yaml"
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set NEXT_PUBLIC_BUILD_ID
@@ -665,17 +685,15 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
- name: Setup Blacksmith Builder
uses: useblacksmith/setup-docker-builder@5241b2e9423e8b1fa37ed6050ecb62d0fb9a4e38 # v1
- name: Extract metadata (tags, labels) for Docker
id: meta-web
- name: Extract metadata (labels) for Docker
id: meta
uses: docker/metadata-action@818d4b7b91585d195f67373fd9cb0332e31a7175 # v4
with:
images: |
ghcr.io/langfuse/langfuse # GitHub
langfuse/langfuse # Docker Hub
ghcr.io/langfuse/${{ matrix.image_name }}
langfuse/${{ matrix.image_name }}
flavor: |
latest=false
tags: |
@@ -686,24 +704,90 @@ jobs:
type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref, '-rc') }}
type=semver,pattern={{major}},enable=${{ !contains(github.ref, '-rc') }}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') && !contains(github.ref, '-rc') }}
- name: Build and push Docker image (web)
- name: Build and push image by digest to GitHub Container Registry (${{ matrix.component }}, ${{ matrix.platform_tag }})
id: build-ghcr
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
with:
context: .
file: ./web/Dockerfile
push: true
tags: ${{ steps.meta-web.outputs.tags }}
labels: ${{ steps.meta-web.outputs.labels }}
platforms: |
linux/amd64
${{ startsWith(github.ref, 'refs/tags/') && 'linux/arm64' || '' }}
- name: Extract metadata (tags, labels) for Docker
id: meta-worker
file: ${{ matrix.dockerfile }}
outputs: type=image,name=ghcr.io/langfuse/${{ matrix.image_name }},push-by-digest=true,name-canonical=true,push=true
labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ matrix.platform }}
provenance: false
sbom: false
- name: Build and push image by digest to Docker Hub (${{ matrix.component }}, ${{ matrix.platform_tag }})
id: build-dockerhub
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
with:
context: .
file: ${{ matrix.dockerfile }}
outputs: type=image,name=langfuse/${{ matrix.image_name }},push-by-digest=true,name-canonical=true,push=true
labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ matrix.platform }}
provenance: false
sbom: false
- name: Record pushed digests
run: |
if [ -z '${{ steps.build-ghcr.outputs.digest }}' ] || [ -z '${{ steps.build-dockerhub.outputs.digest }}' ]; then
echo "Missing registry digest output"
exit 1
fi
mkdir -p "$RUNNER_TEMP/digests/ghcr" "$RUNNER_TEMP/digests/dockerhub"
printf '%s\n' '${{ steps.build-ghcr.outputs.digest }}' > "$RUNNER_TEMP/digests/ghcr/${{ matrix.platform_tag }}.txt"
printf '%s\n' '${{ steps.build-dockerhub.outputs.digest }}' > "$RUNNER_TEMP/digests/dockerhub/${{ matrix.platform_tag }}.txt"
- name: Upload release digests
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: release-digests-${{ matrix.component }}-${{ matrix.platform_tag }}
path: |
${{ runner.temp }}/digests/ghcr/${{ matrix.platform_tag }}.txt
${{ runner.temp }}/digests/dockerhub/${{ matrix.platform_tag }}.txt
if-no-files-found: error
publish-docker-image-release:
needs:
- build-docker-image-release
if: always() && needs.build-docker-image-release.result == 'success' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
environment: "protected branches"
strategy:
fail-fast: false
matrix:
include:
- component: web
image_name: langfuse
- component: worker
image_name: langfuse-worker
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
packages: write
contents: read
steps:
- name: Log in to the GitHub Container registry
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup Blacksmith Builder
uses: useblacksmith/setup-docker-builder@5241b2e9423e8b1fa37ed6050ecb62d0fb9a4e38 # v1
- name: Download release digests
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4
with:
pattern: release-digests-${{ matrix.component }}-*
merge-multiple: true
path: ${{ runner.temp }}/digests
- name: Extract metadata (tags) for GitHub Container Registry
id: meta-ghcr
uses: docker/metadata-action@818d4b7b91585d195f67373fd9cb0332e31a7175 # v4
with:
images: |
ghcr.io/langfuse/langfuse-worker # GitHub
langfuse/langfuse-worker # Docker Hub
images: ghcr.io/langfuse/${{ matrix.image_name }}
flavor: |
latest=false
tags: |
@@ -714,17 +798,87 @@ jobs:
type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref, '-rc') }}
type=semver,pattern={{major}},enable=${{ !contains(github.ref, '-rc') }}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') && !contains(github.ref, '-rc') }}
- name: Build and push Docker image (worker)
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
- name: Extract metadata (tags) for Docker Hub
id: meta-dockerhub
uses: docker/metadata-action@818d4b7b91585d195f67373fd9cb0332e31a7175 # v4
with:
context: .
file: ./worker/Dockerfile
push: true
tags: ${{ steps.meta-worker.outputs.tags }}
labels: ${{ steps.meta-worker.outputs.labels }}
platforms: |
linux/amd64
${{ startsWith(github.ref, 'refs/tags/') && 'linux/arm64' || '' }}
images: langfuse/${{ matrix.image_name }}
flavor: |
latest=false
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref, '-rc') }}
type=semver,pattern={{major}},enable=${{ !contains(github.ref, '-rc') }}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') && !contains(github.ref, '-rc') }}
- name: Publish multi-platform manifest to GitHub Container Registry
run: |
ghcr_tags=()
ghcr_sources=()
while IFS= read -r tag; do
[ -n "$tag" ] || continue
ghcr_tags+=("-t" "$tag")
done <<'EOF'
${{ steps.meta-ghcr.outputs.tags }}
EOF
shopt -s nullglob
for digest_file in "$RUNNER_TEMP"/digests/ghcr/*.txt; do
digest="$(cat "$digest_file")"
ghcr_sources+=("ghcr.io/langfuse/${{ matrix.image_name }}@$digest")
done
if [ "${#ghcr_sources[@]}" -lt 2 ]; then
echo "Expected amd64 and arm64 GHCR digests for ${{ matrix.component }}"
exit 1
fi
docker buildx imagetools create "${ghcr_tags[@]}" "${ghcr_sources[@]}"
- name: Publish multi-platform manifest to Docker Hub
run: |
dockerhub_tags=()
dockerhub_sources=()
while IFS= read -r tag; do
[ -n "$tag" ] || continue
dockerhub_tags+=("-t" "$tag")
done <<'EOF'
${{ steps.meta-dockerhub.outputs.tags }}
EOF
shopt -s nullglob
for digest_file in "$RUNNER_TEMP"/digests/dockerhub/*.txt; do
digest="$(cat "$digest_file")"
dockerhub_sources+=("langfuse/${{ matrix.image_name }}@$digest")
done
if [ "${#dockerhub_sources[@]}" -lt 2 ]; then
echo "Expected amd64 and arm64 Docker Hub digests for ${{ matrix.component }}"
exit 1
fi
docker buildx imagetools create "${dockerhub_tags[@]}" "${dockerhub_sources[@]}"
- name: Inspect published manifests
run: |
ghcr_first_tag="$(printf '%s\n' "${{ steps.meta-ghcr.outputs.tags }}" | sed -n '1p')"
dockerhub_first_tag="$(printf '%s\n' "${{ steps.meta-dockerhub.outputs.tags }}" | sed -n '1p')"
docker buildx imagetools inspect "$ghcr_first_tag"
docker buildx imagetools inspect "$dockerhub_first_tag"
notify-docker-image-release:
needs:
- build-docker-image-release
- publish-docker-image-release
if: always() && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Fail when a release image job failed
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1
- name: Notify Slack
uses: ravsamhq/notify-slack-action@be814b201e233b2dc673608aa46e5447c8ab13f2 # v2
if: always()
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse",
"version": "3.167.2",
"version": "3.167.4",
"author": "engineering@langfuse.com",
"license": "MIT",
"private": true,
@@ -108,7 +108,8 @@
"@types/node-fetch": "^2.6.13",
"@types/react-dom": "19.2.3",
"glob": "^10.5.0",
"qs": "6.14.1"
"qs": "6.14.1",
"path-to-regexp@0.1.12": "0.1.13"
},
"patchedDependencies": {
"next-auth@4.24.13": "patches/next-auth@4.24.13.patch"
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v3.167.2";
export const VERSION = "v3.167.4";
+5 -4
View File
@@ -14,6 +14,7 @@ overrides:
'@types/react-dom': 19.2.3
glob: ^10.5.0
qs: 6.14.1
path-to-regexp@0.1.12: 0.1.13
patchedDependencies:
next-auth@4.24.13:
@@ -9619,8 +9620,8 @@ packages:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
path-to-regexp@0.1.12:
resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==}
path-to-regexp@0.1.13:
resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==}
path-to-regexp@6.3.0:
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
@@ -18602,7 +18603,7 @@ snapshots:
module-details-from-path: 1.0.4
mutexify: 1.4.0
opentracing: 0.14.7
path-to-regexp: 0.1.12
path-to-regexp: 0.1.13
pprof-format: 2.1.0
protobufjs: 7.5.4
retry: 0.13.1
@@ -22143,7 +22144,7 @@ snapshots:
lru-cache: 10.4.3
minipass: 7.1.3
path-to-regexp@0.1.12: {}
path-to-regexp@0.1.13: {}
path-to-regexp@6.3.0: {}
+8 -2
View File
@@ -359,14 +359,18 @@ ensure_clickhouse_running() {
local clickhouse_log="$clickhouse_root/clickhouse.log"
local clickhouse_err="$clickhouse_root/clickhouse.err.log"
local clickhouse_pid="$clickhouse_root/clickhouse.pid"
local -a clickhouse_runner
mkdir -p "$clickhouse_data"
if [ "${EUID:-$(id -u)}" -eq 0 ] && id -u clickhouse >/dev/null 2>&1; then
chown -R clickhouse:clickhouse "$clickhouse_root"
clickhouse_runner=(runuser -u clickhouse --)
else
clickhouse_runner=()
fi
if ! wait_for_http "http://127.0.0.1:$CLICKHOUSE_HTTP_PORT/ping" 1; then
clickhouse-server \
"${clickhouse_runner[@]}" clickhouse-server \
--daemon \
--config-file=/etc/clickhouse-server/config.xml \
--pid-file="$clickhouse_pid" \
@@ -389,7 +393,9 @@ ensure_clickhouse_running() {
clickhouse_user_identifier="$(escape_clickhouse_identifier "$CLICKHOUSE_USER")"
clickhouse-client --host 127.0.0.1 --port "$CLICKHOUSE_NATIVE_PORT" -q "CREATE USER IF NOT EXISTS $clickhouse_user_identifier IDENTIFIED WITH plaintext_password BY '$clickhouse_password_sql'"
clickhouse-client --host 127.0.0.1 --port "$CLICKHOUSE_NATIVE_PORT" -q "GRANT ALL ON *.* TO $clickhouse_user_identifier WITH GRANT OPTION"
if ! clickhouse-client --host 127.0.0.1 --port "$CLICKHOUSE_NATIVE_PORT" -q "GRANT CURRENT GRANTS ON *.* TO $clickhouse_user_identifier" >/dev/null 2>&1; then
clickhouse-client --host 127.0.0.1 --port "$CLICKHOUSE_NATIVE_PORT" -q "GRANT ALL ON *.* TO $clickhouse_user_identifier WITH GRANT OPTION"
fi
}
ensure_minio_running() {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "3.167.2",
"version": "3.167.4",
"private": true,
"license": "MIT",
"engines": {
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v3.167.2";
export const VERSION = "v3.167.4";
@@ -28,6 +28,7 @@ import { api } from "@/src/utils/api";
import { Skeleton } from "@/src/components/ui/skeleton";
import { JSONView } from "@/src/components/ui/CodeJsonViewer";
import { decomposeAggregateScoreKey } from "@/src/features/scores/lib/aggregateScores";
import { cn } from "@/src/utils/tailwind";
type ExperimentGridCellProps = {
projectId: string;
@@ -49,6 +50,7 @@ type ExperimentGridCellProps = {
baselineTraceScores?: ScoreAggregate;
isLoading?: boolean;
columnVisibility?: VisibilityState;
markerClassName?: string;
};
/**
@@ -263,15 +265,27 @@ const MetadataItem = ({
const GroupSection = ({
header,
children,
markerClassName,
}: {
header?: string;
children: React.ReactNode;
markerClassName?: string;
}) => (
<div className="flex shrink-0 flex-col gap-1 px-2 py-1.5">
{header && (
<span className="text-muted-foreground text-[10px] font-semibold uppercase">
{header}
</span>
<div className="flex items-center gap-1.5">
{markerClassName !== undefined && (
<span
className={cn(
"h-3 w-0.5 shrink-0 rounded-full",
markerClassName || "bg-transparent",
)}
/>
)}
<span className="text-muted-foreground text-[10px] font-semibold uppercase">
{header}
</span>
</div>
)}
{children}
</div>
@@ -300,6 +314,7 @@ export const ExperimentGridCell = ({
baselineTraceScores,
isLoading = false,
columnVisibility = {},
markerClassName,
}: ExperimentGridCellProps) => {
const scoreDiffs = useMemo(
() =>
@@ -492,45 +507,50 @@ export const ExperimentGridCell = ({
.filter((section) => section.content !== null);
return (
<div className="flex h-full w-full flex-col overflow-hidden">
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto">
{sectionsToRender.map((section, index) => {
const { row, content } = section;
const isLast = index === sectionsToRender.length - 1;
<div className="flex h-full min-h-0 w-full flex-1 flex-col overflow-y-auto">
{sectionsToRender.map((section, index) => {
const { row, content } = section;
const isFirst = index === 0;
const isLast = index === sectionsToRender.length - 1;
// Output section - special handling for MemoizedIOTableCell
if (row.accessorKey === "output" && row.cell) {
return (
<Fragment key={row.accessorKey}>
<GroupSection header={row.header}>
{row.cell({ data: cellData })}
</GroupSection>
{!isLast && <Separator />}
</Fragment>
);
}
// Output section - special handling for MemoizedIOTableCell
if (row.accessorKey === "output" && row.cell) {
return (
<Fragment key={row.accessorKey}>
<GroupSection
header={row.header}
markerClassName={isFirst ? markerClassName : undefined}
>
{row.cell({ data: cellData })}
</GroupSection>
{!isLast && <Separator />}
</Fragment>
);
}
// Groups with children (metadata, scores)
if (row.children && content) {
return (
<Fragment key={row.accessorKey}>
<GroupSection header={row.header}>
<div className="flex flex-col gap-0.5">
{(content as CellRowDef<GridCellData>[]).map((child) => (
<div key={child.accessorKey}>
{child.cell?.({ data: cellData })}
</div>
))}
</div>
</GroupSection>
{!isLast && <Separator />}
</Fragment>
);
}
// Groups with children (metadata, scores)
if (row.children && content) {
return (
<Fragment key={row.accessorKey}>
<GroupSection
header={row.header}
markerClassName={isFirst ? markerClassName : undefined}
>
<div className="flex flex-col gap-0.5">
{(content as CellRowDef<GridCellData>[]).map((child) => (
<div key={child.accessorKey}>
{child.cell?.({ data: cellData })}
</div>
))}
</div>
</GroupSection>
{!isLast && <Separator />}
</Fragment>
);
}
return null;
})}
</div>
return null;
})}
</div>
);
};
@@ -6,7 +6,10 @@ import {
ExperimentGridCell,
ExperimentGridCellEmpty,
} from "./ExperimentGridCell";
import { type ExperimentItemsTableRow, getExperimentColor } from "./types";
import {
type ExperimentItemsTableRow,
getExperimentColorStyles,
} from "./types";
import { useMemo } from "react";
import { type RowHeight } from "@/src/components/table/data-table-row-height-switch";
import {
@@ -79,8 +82,8 @@ export const ExperimentGridView = ({
const isBaseline = index === 0;
const expInfo = experimentNames.find((e) => e.experimentId === expId);
const expName = expInfo?.experimentName ?? expId.slice(0, 8);
const colorClass = useExperimentColors
? getExperimentColor(expId, allExperimentIds)
const colorStyles = useExperimentColors
? getExperimentColorStyles(expId, allExperimentIds)
: undefined;
return {
@@ -88,16 +91,18 @@ export const ExperimentGridView = ({
id: expId,
header: () => (
<div className="flex items-center gap-2">
<span className={cn("truncate font-medium", colorClass)}>
<span
className={cn("truncate font-medium", colorStyles?.textClass)}
>
{expName}
</span>
{isBaseline && useExperimentColors && (
{useExperimentColors && (
<Badge
variant="secondary"
variant="outline"
size="sm"
className="shrink-0 font-medium"
className={cn("shrink-0 font-medium", colorStyles?.badgeClass)}
>
Baseline
{isBaseline ? "Baseline" : "Comp"}
</Badge>
)}
</div>
@@ -144,6 +149,7 @@ export const ExperimentGridView = ({
baselineScores={baselineData?.observationScores}
baselineTraceScores={baselineData?.traceScores}
columnVisibility={columnVisibility}
markerClassName={colorStyles?.markerClass}
/>
);
},
@@ -212,6 +218,7 @@ export const ExperimentGridView = ({
customRowHeights={GRID_VIEW_ROW_HEIGHTS}
topAlignCells
peekView={peekView}
columnVisibility={columnVisibility}
/>
);
};
@@ -40,7 +40,7 @@ import {
type ExperimentItemsTableProps,
type ExperimentItemData,
type ExperimentOutputData,
getExperimentColor,
getExperimentColorStyles,
} from "./types";
import { MemoizedIOTableCell } from "@/src/components/ui/IOTableCell";
import {
@@ -101,21 +101,28 @@ const StackedExperimentCell = ({
>
{allExperimentIds.map((experimentId) => {
const exp = experimentsById.get(experimentId);
const colorStyles = getExperimentColorStyles(
experimentId,
colorExperimentIds ?? allExperimentIds,
);
const content = exp ? renderValue(exp) : null;
return (
<div
key={experimentId}
className={cn(
"flex min-h-0 items-start overflow-hidden px-2",
getExperimentColor(
experimentId,
colorExperimentIds ?? allExperimentIds,
),
)}
className="flex min-h-0 items-start overflow-hidden py-0.5 pr-2 pl-1.5"
>
{exp ? (
renderValue(exp)
{content ? (
<>
<span
className={cn(
"mt-0.5 mr-2 block h-4 w-0.5 shrink-0 rounded-full",
colorStyles.markerClass,
)}
/>
{content}
</>
) : (
<span className="text-muted-foreground">-</span>
<span className="text-muted-foreground"></span>
)}
</div>
);
@@ -154,32 +161,41 @@ const StackedOutputCell = ({
>
{allExperimentIds.map((experimentId) => {
const out = outputsByExperimentId.get(experimentId);
const colorStyles = getExperimentColorStyles(
experimentId,
colorExperimentIds ?? allExperimentIds,
);
return (
<div
key={experimentId}
className={cn(
"flex min-h-0 items-start overflow-hidden",
getExperimentColor(
experimentId,
colorExperimentIds ?? allExperimentIds,
),
)}
className="flex min-h-0 items-start overflow-hidden py-0.5 pr-1 pl-1.5"
>
{isLoading ? (
<MemoizedIOTableCell
isLoading={true}
data={null}
singleLine={singleLine}
/>
<div className="flex min-w-0 items-start">
<span className="bg-muted mt-0.5 mr-2 block h-4 w-0.5 shrink-0 rounded-full" />
<MemoizedIOTableCell
isLoading={true}
data={null}
singleLine={singleLine}
/>
</div>
) : out?.output ? (
<MemoizedIOTableCell
isLoading={false}
data={out.output}
singleLine={singleLine}
className="bg-accent-light-green"
/>
<div className="flex min-w-0 items-start">
<span
className={cn(
"mt-0.5 mr-2 block h-4 w-0.5 shrink-0 rounded-full",
colorStyles.markerClass,
)}
/>
<MemoizedIOTableCell
isLoading={false}
data={out.output}
singleLine={singleLine}
className="bg-accent-light-green"
/>
</div>
) : (
<span className="text-muted-foreground px-2 py-1">-</span>
<span className="text-muted-foreground px-2 py-1"></span>
)}
</div>
);
@@ -3,14 +3,41 @@ import { type VisibilityState } from "@tanstack/react-table";
import { type ReactNode } from "react";
// Shared font color palette for experiment rows/columns
export const EXPERIMENT_COLORS = [
"text-dark-gray", // Baseline - index 0
"text-blue-700", // Comparison 1
"text-pink-700", // Comparison 2
"text-purple-700", // Comparison 3
"text-orange-700", // Comparison 4
export const EXPERIMENT_COLOR_STYLES = [
{
textClass: "text-dark-gray",
markerClass: "bg-slate-500 dark:bg-slate-400",
badgeClass:
"border-slate-400/80 bg-slate-100/70 text-slate-700 dark:border-slate-500/70 dark:bg-slate-900/60 dark:text-slate-300",
}, // Baseline - index 0
{
textClass: "text-blue-700 dark:text-blue-300",
markerClass: "bg-blue-500/80 dark:bg-blue-400/80",
badgeClass:
"border-blue-500/45 bg-blue-500/12 text-blue-700 dark:border-blue-400/45 dark:bg-blue-400/15 dark:text-blue-300",
}, // Comparison 1
{
textClass: "text-violet-700 dark:text-violet-300",
markerClass: "bg-violet-500/80 dark:bg-violet-400/80",
badgeClass:
"border-violet-500/45 bg-violet-500/12 text-violet-700 dark:border-violet-400/45 dark:bg-violet-400/15 dark:text-violet-300",
}, // Comparison 2
{
textClass: "text-teal-700 dark:text-teal-300",
markerClass: "bg-teal-500/80 dark:bg-teal-400/80",
badgeClass:
"border-teal-500/45 bg-teal-500/12 text-teal-700 dark:border-teal-400/45 dark:bg-teal-400/15 dark:text-teal-300",
}, // Comparison 3
{
textClass: "text-amber-700 dark:text-amber-300",
markerClass: "bg-amber-500/80 dark:bg-amber-400/80",
badgeClass:
"border-amber-500/45 bg-amber-500/12 text-amber-700 dark:border-amber-400/45 dark:bg-amber-400/15 dark:text-amber-300",
}, // Comparison 4
] as const;
export type ExperimentColorStyle = (typeof EXPERIMENT_COLOR_STYLES)[number];
/**
* Get the text color class for an experiment based on its index.
*/
@@ -18,8 +45,19 @@ export const getExperimentColor = (
experimentId: string,
allExperimentIds: string[],
): string => {
const styles = getExperimentColorStyles(experimentId, allExperimentIds);
return styles.textClass;
};
export const getExperimentColorStyles = (
experimentId: string,
allExperimentIds: string[],
): ExperimentColorStyle => {
const index = allExperimentIds.indexOf(experimentId);
return EXPERIMENT_COLORS[index % EXPERIMENT_COLORS.length];
return (
EXPERIMENT_COLOR_STYLES[index % EXPERIMENT_COLOR_STYLES.length] ??
EXPERIMENT_COLOR_STYLES[0]
);
};
export type ExperimentsTableRow = {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "worker",
"version": "3.167.2",
"version": "3.167.4",
"description": "",
"license": "MIT",
"private": true,
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v3.167.2";
export const VERSION = "v3.167.4";
@@ -837,8 +837,9 @@ async function processExperimentBackfill(
if (datasetRunItems.length === 0) {
logger.info(
"[EXPERIMENT BACKFILL] No dataset run items to process, skipping",
"[EXPERIMENT BACKFILL] No dataset run items to process, advancing cursor",
);
await updateBackfillTimestamp(upperBound);
return;
}