feat(core): Add agent-evals PostHog feature flag and gating (no-changelog) (#34651)

This commit is contained in:
Arvin A
2026-07-22 10:16:40 +00:00
committed by GitHub
parent ee029b8df9
commit 85b1188d1d
11 changed files with 137 additions and 1 deletions
@@ -272,6 +272,13 @@ export interface FrontendSettings {
* `084_eval_collections` flag would otherwise never resolve.
*/
collectionsEnabled: boolean;
/**
* Operator override (`N8N_AGENT_EVALS_ENABLED`) that force-enables the
* agent-evals surface. Surfaced here so the frontend gate works even when
* the in-browser PostHog client is disabled (telemetry off), where the
* `101_agent_evals` flag would otherwise never resolve.
*/
agentEvalsEnabled: boolean;
};
/** Backend modules that were initialized during startup. */
+2
View File
@@ -560,6 +560,8 @@ export {
type EvalVersionsResponse,
} from './schemas/eval-collections.schema';
export { AGENT_EVALS_FLAG } from './schemas/agent-evals.schema';
export {
aiInsightsStatusSchema,
aiInsightsPayloadSchema,
@@ -0,0 +1,7 @@
// PostHog rollout flag id gating the agent-evals feature surface. Every new
// agent-eval endpoint + frontend entry point consults this; the flag-off
// cohort sees no agent-eval surface at all. Single source of truth shared
// between FE and BE so the two cannot drift. Follows the `NNN_<feature>`
// numeric-prefix convention used by every other n8n PostHog flag (next
// available after `100_n8n_credits_credential_selection`).
export const AGENT_EVALS_FLAG = '101_agent_evals';
@@ -22,4 +22,23 @@ export class EvaluationConfig {
*/
@Env('N8N_EVAL_COLLECTIONS_ENABLED')
collectionsEnabled: boolean = false;
/**
* Force-enable the agent-evals feature surface.
*
* Acts as an operator-level override of the `101_agent_evals` PostHog
* rollout flag. When `true`, every agent-eval endpoint + frontend entry
* point is available regardless of PostHog cohort. When `false` (default),
* PostHog remains the source of truth.
*
* Useful for:
* - Local development without PostHog wiring.
* - QA environments that need the feature on for a smoke run.
* - Self-hosted deployments that want the feature without PostHog.
*
* Cannot force-disable: setting this to `false` falls back to PostHog,
* not a kill-switch. Use PostHog itself to disable a rolled-out flag.
*/
@Env('N8N_AGENT_EVALS_ENABLED')
agentEvalsEnabled: boolean = false;
}
+1
View File
@@ -468,6 +468,7 @@ describe('GlobalConfig', () => {
},
evaluation: {
collectionsEnabled: false,
agentEvalsEnabled: false,
},
generic: {
timezone: 'America/New_York',
@@ -219,6 +219,7 @@ describe('PostHog', () => {
// Mutated per test; reset so test ordering doesn't leak override
// state into unrelated cases.
globalConfig.evaluation.collectionsEnabled = false;
globalConfig.evaluation.agentEvalsEnabled = false;
});
it('force-enables the eval-collections flag when N8N_EVAL_COLLECTIONS_ENABLED is set', async () => {
@@ -257,6 +258,30 @@ describe('PostHog', () => {
expect(flags).toEqual({ '084_eval_collections': true });
});
it('force-enables the agent-evals flag when N8N_AGENT_EVALS_ENABLED is set', async () => {
(PostHog.prototype.evaluateFlags as Mock).mockResolvedValue(mockEvaluatedFlags({}));
globalConfig.evaluation.agentEvalsEnabled = true;
const ph = new PostHogClient(instanceSettings, globalConfig);
await ph.init();
const flags = await ph.getFeatureFlags({ id: userId, createdAt });
expect(flags).toMatchObject({ '101_agent_evals': true });
});
it('falls back to the agent-evals override when PostHog throws', async () => {
(PostHog.prototype.evaluateFlags as Mock).mockRejectedValue(new Error('posthog down'));
globalConfig.evaluation.agentEvalsEnabled = true;
const ph = new PostHogClient(instanceSettings, globalConfig);
await ph.init();
const flags = await ph.getFeatureFlags({ id: userId, createdAt });
expect(flags).toEqual({ '101_agent_evals': true });
});
});
});
});
+4 -1
View File
@@ -1,4 +1,4 @@
import { EVAL_COLLECTIONS_FLAG } from '@n8n/api-types';
import { AGENT_EVALS_FLAG, EVAL_COLLECTIONS_FLAG } from '@n8n/api-types';
import { GlobalConfig } from '@n8n/config';
import type { PublicUser } from '@n8n/db';
import { Service } from '@n8n/di';
@@ -168,6 +168,9 @@ export class PostHogClient {
if (this.globalConfig.evaluation.collectionsEnabled) {
overrides[EVAL_COLLECTIONS_FLAG] = true;
}
if (this.globalConfig.evaluation.agentEvalsEnabled) {
overrides[AGENT_EVALS_FLAG] = true;
}
return Object.keys(overrides).length === 0 ? flags : { ...flags, ...overrides };
}
}
@@ -420,6 +420,7 @@ export class FrontendService {
evaluation: {
quota: this.licenseState.getMaxWorkflowsWithEvaluations(),
collectionsEnabled: this.globalConfig.evaluation.collectionsEnabled,
agentEvalsEnabled: this.globalConfig.evaluation.agentEvalsEnabled,
},
activeModules: this.moduleRegistry.getActiveModules(),
canvasOnly: this.globalConfig.canvasOnly,
@@ -184,6 +184,7 @@ export const defaultSettings: FrontendSettings = {
evaluation: {
quota: 0,
collectionsEnabled: false,
agentEvalsEnabled: false,
},
activeModules: [],
canvasOnly: false,
@@ -0,0 +1,41 @@
import { describe, expect, it, vi } from 'vitest';
import { useAgentEvalsFlag } from './useAgentEvalsFlag';
const settingsState = { agentEvalsEnabled: false };
const posthogState = { enabled: false };
vi.mock('@/app/stores/settings.store', () => ({
useSettingsStore: () => ({
settings: { evaluation: { agentEvalsEnabled: settingsState.agentEvalsEnabled } },
}),
}));
vi.mock('@/app/stores/posthog.store', () => ({
usePostHog: () => ({ isFeatureEnabled: () => posthogState.enabled }),
}));
describe('useAgentEvalsFlag', () => {
it('is enabled via the backend operator override even when PostHog is off', () => {
// The telemetry-off case: the in-browser PostHog client never initializes,
// so the flag must come from the settings-provided override.
settingsState.agentEvalsEnabled = true;
posthogState.enabled = false;
expect(useAgentEvalsFlag().value).toBe(true);
});
it('is enabled via the PostHog cohort flag when the override is off', () => {
settingsState.agentEvalsEnabled = false;
posthogState.enabled = true;
expect(useAgentEvalsFlag().value).toBe(true);
});
it('is disabled when neither signal is set', () => {
settingsState.agentEvalsEnabled = false;
posthogState.enabled = false;
expect(useAgentEvalsFlag().value).toBe(false);
});
});
@@ -0,0 +1,29 @@
import { AGENT_EVALS_FLAG } from '@n8n/api-types';
import { computed } from 'vue';
import { usePostHog } from '@/app/stores/posthog.store';
import { useSettingsStore } from '@/app/stores/settings.store';
/**
* Frontend gate for the agent-evals feature surface, matching the
* `101_agent_evals` flag the backend consults to gate its routes. It combines
* two independent signals:
*
* - `settings.evaluation.agentEvalsEnabled` — the backend-provided operator
* override (`N8N_AGENT_EVALS_ENABLED`). Delivered in the settings payload,
* so it works even when the in-browser PostHog client never initializes
* (telemetry off), where the flag would otherwise stay false.
* - the PostHog client flag — carries per-cohort rollout when telemetry is on.
*
* Coerces to a strict boolean so `v-if="isAgentEvalsEnabled"` never
* undefined-flickers during the initial flag-fetch frame.
*/
export const useAgentEvalsFlag = () => {
const postHog = usePostHog();
const settingsStore = useSettingsStore();
return computed(
() =>
settingsStore.settings.evaluation?.agentEvalsEnabled === true ||
postHog.isFeatureEnabled(AGENT_EVALS_FLAG) === true,
);
};