diff --git a/packages/@n8n/api-types/src/frontend-settings.ts b/packages/@n8n/api-types/src/frontend-settings.ts index 6001957f597..64acdb10449 100644 --- a/packages/@n8n/api-types/src/frontend-settings.ts +++ b/packages/@n8n/api-types/src/frontend-settings.ts @@ -66,6 +66,7 @@ export interface IEnterpriseSettings { customRoles: boolean; personalSpacePolicy: boolean; dataRedaction: boolean; + otelCustomSpanAttributes: boolean; } export interface FrontendSettings { diff --git a/packages/@n8n/backend-common/src/license-state.ts b/packages/@n8n/backend-common/src/license-state.ts index 6c05688685a..8b1e05c87b0 100644 --- a/packages/@n8n/backend-common/src/license-state.ts +++ b/packages/@n8n/backend-common/src/license-state.ts @@ -194,6 +194,10 @@ export class LicenseState { return this.isLicensed(['feat:saml', 'feat:oidc']); } + isOtelCustomSpanAttributesLicensed() { + return this.isLicensed(LICENSE_FEATURES.OTEL_CUSTOM_SPAN_ATTRIBUTES); + } + // -------------------- // integers // -------------------- diff --git a/packages/@n8n/constants/src/index.ts b/packages/@n8n/constants/src/index.ts index 2af7b197f4a..ac464fa80c3 100644 --- a/packages/@n8n/constants/src/index.ts +++ b/packages/@n8n/constants/src/index.ts @@ -45,6 +45,7 @@ export const LICENSE_FEATURES = { TOKEN_EXCHANGE: 'feat:tokenExchange', DATA_REDACTION: 'feat:dataRedaction', N8N_PACKAGES: 'feat:n8nPackages', + OTEL_CUSTOM_SPAN_ATTRIBUTES: 'feat:otel:customSpanAttributes', } as const; export const LICENSE_QUOTAS = { diff --git a/packages/cli/src/controllers/e2e.controller.ts b/packages/cli/src/controllers/e2e.controller.ts index fea08e27d80..d7349e94c1d 100644 --- a/packages/cli/src/controllers/e2e.controller.ts +++ b/packages/cli/src/controllers/e2e.controller.ts @@ -130,6 +130,7 @@ export class E2EController { [LICENSE_FEATURES.TOKEN_EXCHANGE]: false, [LICENSE_FEATURES.DATA_REDACTION]: false, [LICENSE_FEATURES.N8N_PACKAGES]: false, + [LICENSE_FEATURES.OTEL_CUSTOM_SPAN_ATTRIBUTES]: false, }; private static readonly numericFeaturesDefaults: Record = { diff --git a/packages/cli/src/modules/otel/__tests__/otel-lifecycle-handler.test.ts b/packages/cli/src/modules/otel/__tests__/otel-lifecycle-handler.test.ts index 98f1df634e5..8b5e36a1d4d 100644 --- a/packages/cli/src/modules/otel/__tests__/otel-lifecycle-handler.test.ts +++ b/packages/cli/src/modules/otel/__tests__/otel-lifecycle-handler.test.ts @@ -1,4 +1,4 @@ -import type { Logger } from '@n8n/backend-common'; +import type { LicenseState, Logger } from '@n8n/backend-common'; import type { NodeExecuteAfterContext, NodeExecuteBeforeContext, @@ -45,6 +45,7 @@ describe('OtelLifecycleHandler', () => { let config = makeOtelConfig(); const ownershipService = mock(); const logger = mock(); + const licenseState = mock(); let handler: OtelLifecycleHandler; const parentTracingContext: TracingContext = { @@ -82,7 +83,9 @@ describe('OtelLifecycleHandler', () => { config, ownershipService, logger, + licenseState, ); + licenseState.isOtelCustomSpanAttributesLicensed.mockReturnValue(true); tracer.startWorkflow.mockReturnValue(generatedSpanContext); ownershipService.getWorkflowProjectCached.mockResolvedValue({ id: 'proj-default' } as never); }); @@ -121,6 +124,32 @@ describe('OtelLifecycleHandler', () => { ); }); + it('should omit project and workflow customAttributes when custom OTel span attributes are not licensed', async () => { + licenseState.isOtelCustomSpanAttributesLicensed.mockReturnValue(false); + traceContextService.get.mockResolvedValueOnce(undefined); + ownershipService.getWorkflowProjectCached.mockResolvedValueOnce({ + id: 'proj-1', + customTelemetryTags: [{ key: 'env', value: 'production' }], + } as never); + + await handler.onWorkflowStart({ + ...baseCtx, + workflow: { + ...baseCtx.workflow, + settings: { + customTelemetryTags: [{ key: 'workflowName', value: 'Workflow Name' }], + }, + }, + }); + + expect(tracer.startWorkflow).toHaveBeenCalledWith( + expect.objectContaining({ + project: { id: 'proj-1', customAttributes: undefined }, + workflow: expect.objectContaining({ customAttributes: undefined }), + }), + ); + }); + it('should pass undefined customAttributes when project has no telemetry tags', async () => { traceContextService.get.mockResolvedValueOnce(undefined); ownershipService.getWorkflowProjectCached.mockResolvedValueOnce({ @@ -310,6 +339,7 @@ describe('OtelLifecycleHandler', () => { let config = makeOtelConfig(); const ownershipService = mock(); const logger = mock(); + const licenseState = mock(); let handler: OtelLifecycleHandler; const prePauseContext: TracingContext = { @@ -328,7 +358,9 @@ describe('OtelLifecycleHandler', () => { config, ownershipService, logger, + licenseState, ); + licenseState.isOtelCustomSpanAttributesLicensed.mockReturnValue(true); tracer.startWorkflow.mockReturnValue(resumedSpanContext); ownershipService.getWorkflowProjectCached.mockResolvedValue({ id: 'proj-default' } as never); }); @@ -378,6 +410,40 @@ describe('OtelLifecycleHandler', () => { ); }); + it('should omit project customAttributes on resume when custom OTel span attributes are not licensed', async () => { + licenseState.isOtelCustomSpanAttributesLicensed.mockReturnValue(false); + traceContextService.get.mockResolvedValueOnce(undefined); + ownershipService.getWorkflowProjectCached.mockResolvedValueOnce({ + id: 'resume-proj-tags', + customTelemetryTags: [{ key: 'env', value: 'staging' }], + } as never); + + await handler.onWorkflowResume({ + type: 'workflowExecuteResume', + workflow: { + id: 'wf-1', + name: 'Test', + versionId: 'v1', + nodes: [], + connections: {}, + settings: { customTelemetryTags: [{ key: 'workflowName', value: 'Workflow Name' }] }, + }, + workflowInstance: undefined as never, + executionData: undefined as never, + executionId: 'exec-resume-tags', + } as never); + + expect(tracer.startWorkflow).toHaveBeenCalledWith( + expect.objectContaining({ + project: { + id: 'resume-proj-tags', + customAttributes: undefined, + }, + workflow: expect.objectContaining({ customAttributes: undefined }), + }), + ); + }); + it('should start workflow span without project if project lookup fails on resume', async () => { ownershipService.getWorkflowProjectCached.mockRejectedValueOnce(new Error('DB error')); @@ -433,6 +499,7 @@ describe('OtelLifecycleHandler', () => { let config = makeOtelConfig(); const ownershipService = mock(); const logger = mock(); + const licenseState = mock(); let handler: OtelLifecycleHandler; beforeEach(() => { @@ -444,6 +511,7 @@ describe('OtelLifecycleHandler', () => { config, ownershipService, logger, + licenseState, ); }); @@ -508,6 +576,7 @@ describe('OtelLifecycleHandler', () => { let config = makeOtelConfig(); const ownershipService = mock(); const logger = mock(); + const licenseState = mock(); let handler: OtelLifecycleHandler; const node = { id: 'n1', name: 'Node1', type: 'test', typeVersion: 1 }; @@ -549,7 +618,9 @@ describe('OtelLifecycleHandler', () => { config, ownershipService, logger, + licenseState, ); + licenseState.isOtelCustomSpanAttributesLicensed.mockReturnValue(true); }); it('should skip node spans when includeNodeSpans is false', () => { @@ -560,6 +631,7 @@ describe('OtelLifecycleHandler', () => { config, ownershipService, logger, + licenseState, ); handler.onNodeStart(makeStartCtx()); @@ -619,6 +691,20 @@ describe('OtelLifecycleHandler', () => { ); }); + it('should omit node customAttributes when custom OTel span attributes are not licensed', () => { + licenseState.isOtelCustomSpanAttributesLicensed.mockReturnValue(false); + + handler.onNodeEnd( + makeEndCtx({ + metadata: { tracing: { 'llm.model': 'gpt-4o', 'llm.tokens': 500 } }, + } as unknown as Partial), + ); + + expect(tracer.endNode).toHaveBeenCalledWith( + expect.objectContaining({ customAttributes: undefined }), + ); + }); + it('should forward taskData.error to tracer.endNode', () => { const error = new Error('node failure'); handler.onNodeEnd( @@ -642,6 +728,7 @@ describe('productionExecutionsOnly filter', () => { let config = makeOtelConfig(); const ownershipService = mock(); const logger = mock(); + const licenseState = mock(); let handler: OtelLifecycleHandler; const inactiveWorkflow = { @@ -714,7 +801,9 @@ describe('productionExecutionsOnly filter', () => { config, ownershipService, logger, + licenseState, ); + licenseState.isOtelCustomSpanAttributesLicensed.mockReturnValue(true); }); it('should skip all tracing for an inactive workflow when productionExecutionsOnly is true', async () => { diff --git a/packages/cli/src/modules/otel/__tests__/support/otel-integration-utils.ts b/packages/cli/src/modules/otel/__tests__/support/otel-integration-utils.ts index 0c53bfcb65c..61871bcfa3f 100644 --- a/packages/cli/src/modules/otel/__tests__/support/otel-integration-utils.ts +++ b/packages/cli/src/modules/otel/__tests__/support/otel-integration-utils.ts @@ -1,5 +1,6 @@ -import { ModuleRegistry } from '@n8n/backend-common'; +import { LicenseState, ModuleRegistry } from '@n8n/backend-common'; import { testDb, testModules } from '@n8n/backend-test-utils'; +import { LICENSE_FEATURES } from '@n8n/constants'; import type { WorkflowEntity } from '@n8n/db'; import { ExecutionRepository } from '@n8n/db'; import { Container } from '@n8n/di'; @@ -46,6 +47,10 @@ export async function initOtelTestEnvironment() { await testModules.loadModules(['otel']); await testDb.init(); await Container.get(ModuleRegistry).initModules('main'); + Container.get(LicenseState).setLicenseProvider({ + isLicensed: (feature) => feature === LICENSE_FEATURES.OTEL_CUSTOM_SPAN_ATTRIBUTES, + getValue: () => undefined, + }); const distNodes = loadNodesFromDist([ 'n8n-nodes-base.executeWorkflow', 'n8n-nodes-base.executeWorkflowTrigger', diff --git a/packages/cli/src/modules/otel/otel-lifecycle-handler.ts b/packages/cli/src/modules/otel/otel-lifecycle-handler.ts index 91e3e0e0b94..559af996619 100644 --- a/packages/cli/src/modules/otel/otel-lifecycle-handler.ts +++ b/packages/cli/src/modules/otel/otel-lifecycle-handler.ts @@ -6,7 +6,7 @@ import type { NodeExecuteBeforeContext, NodeExecuteAfterContext, } from '@n8n/decorators'; -import { Logger } from '@n8n/backend-common'; +import { LicenseState, Logger } from '@n8n/backend-common'; import { Service } from '@n8n/di'; import type { ICustomTelemetryTag, IWorkflowBase } from 'n8n-workflow'; @@ -43,6 +43,7 @@ export class OtelLifecycleHandler { private readonly config: OtelConfig, private readonly ownershipService: OwnershipService, private readonly logger: Logger, + private readonly licenseState: LicenseState, ) {} private isPublishedWorkflow(workflow: IWorkflowBase): boolean { @@ -77,7 +78,7 @@ export class OtelLifecycleHandler { project: project ? { id: project.id, - customAttributes: buildProjectCustomAttributes(project.customTelemetryTags), + customAttributes: this.buildProjectCustomAttributes(project.customTelemetryTags), } : undefined, workflow: { @@ -117,7 +118,7 @@ export class OtelLifecycleHandler { project: project ? { id: project.id, - customAttributes: buildProjectCustomAttributes(project.customTelemetryTags), + customAttributes: this.buildProjectCustomAttributes(project.customTelemetryTags), } : undefined, workflow: { @@ -166,27 +167,26 @@ export class OtelLifecycleHandler { const node = ctx.workflow.nodes.find((n) => n.name === ctx.nodeName); if (!node) return; - const customAttributes = ctx.taskData.metadata?.tracing - ? Object.fromEntries( - Object.entries(ctx.taskData.metadata.tracing).map(([key, value]) => [key, String(value)]), - ) - : undefined; - this.tracer.endNode({ executionId: ctx.executionId, node, inputItemCount: countInputItems(ctx), outputItemCount: countOutputItems(ctx.taskData.data), error: ctx.taskData.error ?? undefined, - customAttributes, + customAttributes: this.buildNodeCustomAttributes(ctx), }); } + private areCustomSpanAttributesLicensed(): boolean { + return this.licenseState.isOtelCustomSpanAttributesLicensed(); + } + private buildWorkflowCustomAttributes( ctx: WorkflowExecuteBeforeContext | WorkflowExecuteResumeContext, ): CustomAttributes | undefined { const tags = getCustomTelemetryTags(ctx.workflow.settings?.customTelemetryTags); if (!tags?.length) return; + if (!this.areCustomSpanAttributesLicensed()) return; const customAttributes: CustomAttributes = {}; @@ -201,17 +201,28 @@ export class OtelLifecycleHandler { return customAttributes; } -} -function buildProjectCustomAttributes( - tags: Array<{ key: string; value: string }>, -): Record | undefined { - if (!tags?.length) return undefined; - const attrs: Record = {}; - for (const { key, value } of tags) { - attrs[key] = value; + private buildProjectCustomAttributes( + tags: Array<{ key: string; value: string }> | undefined, + ): Record | undefined { + if (!this.areCustomSpanAttributesLicensed()) return undefined; + if (!tags?.length) return undefined; + + const attrs: Record = {}; + for (const { key, value } of tags) { + attrs[key] = value; + } + return attrs; + } + + private buildNodeCustomAttributes(ctx: NodeExecuteAfterContext): CustomAttributes | undefined { + if (!ctx.taskData.metadata?.tracing) return undefined; + if (!this.areCustomSpanAttributesLicensed()) return undefined; + + return Object.fromEntries( + Object.entries(ctx.taskData.metadata.tracing).map(([key, value]) => [key, String(value)]), + ); } - return attrs; } export function countOutputItems(data: NodeExecuteAfterContext['taskData']['data']): number { diff --git a/packages/cli/src/services/__tests__/frontend.service.test.ts b/packages/cli/src/services/__tests__/frontend.service.test.ts index 84ab71e8b40..265243c2541 100644 --- a/packages/cli/src/services/__tests__/frontend.service.test.ts +++ b/packages/cli/src/services/__tests__/frontend.service.test.ts @@ -158,6 +158,7 @@ describe('FrontendService', () => { const licenseState = mock({ isOidcLicensed: jest.fn().mockReturnValue(false), isMFAEnforcementLicensed: jest.fn().mockReturnValue(false), + isOtelCustomSpanAttributesLicensed: jest.fn().mockReturnValue(false), getMaxWorkflowsWithEvaluations: jest.fn().mockReturnValue(0), }); @@ -328,6 +329,15 @@ describe('FrontendService', () => { // it to 4. expect(settings.evaluationConcurrencyLimit).toBe(4); }); + + it('should surface whether custom OpenTelemetry span attributes are licensed', async () => { + licenseState.isOtelCustomSpanAttributesLicensed.mockReturnValue(true); + + const { service } = createMockService(); + const settings = await service.getSettings(); + + expect(settings.enterprise.otelCustomSpanAttributes).toBe(true); + }); }); describe('getPublicSettings', () => { diff --git a/packages/cli/src/services/frontend.service.ts b/packages/cli/src/services/frontend.service.ts index 20745410a86..3e7ffcb28fd 100644 --- a/packages/cli/src/services/frontend.service.ts +++ b/packages/cli/src/services/frontend.service.ts @@ -348,6 +348,7 @@ export class FrontendService { customRoles: false, personalSpacePolicy: false, dataRedaction: false, + otelCustomSpanAttributes: false, }, mfa: { enabled: false, @@ -506,6 +507,7 @@ export class FrontendService { customRoles: this.licenseState.isCustomRolesLicensed(), personalSpacePolicy: this.licenseState.isPersonalSpacePolicyLicensed(), dataRedaction: this.licenseState.isDataRedactionLicensed(), + otelCustomSpanAttributes: this.licenseState.isOtelCustomSpanAttributesLicensed(), }); if (this.license.isLdapEnabled()) { diff --git a/packages/frontend/editor-ui/src/__tests__/defaults.ts b/packages/frontend/editor-ui/src/__tests__/defaults.ts index 4d2c182e96f..94c5f42f9a8 100644 --- a/packages/frontend/editor-ui/src/__tests__/defaults.ts +++ b/packages/frontend/editor-ui/src/__tests__/defaults.ts @@ -55,6 +55,7 @@ export const defaultSettings: FrontendSettings = { customRoles: false, personalSpacePolicy: false, dataRedaction: false, + otelCustomSpanAttributes: false, }, executionMode: 'regular', isMultiMain: false, diff --git a/packages/frontend/editor-ui/src/__tests__/mocks.ts b/packages/frontend/editor-ui/src/__tests__/mocks.ts index 6018f680b13..75c79330993 100644 --- a/packages/frontend/editor-ui/src/__tests__/mocks.ts +++ b/packages/frontend/editor-ui/src/__tests__/mocks.ts @@ -294,6 +294,7 @@ export function createMockEnterpriseSettings( customRoles: false, personalSpacePolicy: false, dataRedaction: false, + otelCustomSpanAttributes: false, ...overrides, // Override with any passed properties }; } diff --git a/packages/frontend/editor-ui/src/app/components/WorkflowSettings/WorkflowSettings.test.ts b/packages/frontend/editor-ui/src/app/components/WorkflowSettings/WorkflowSettings.test.ts index 4214e761de4..434ba870d83 100644 --- a/packages/frontend/editor-ui/src/app/components/WorkflowSettings/WorkflowSettings.test.ts +++ b/packages/frontend/editor-ui/src/app/components/WorkflowSettings/WorkflowSettings.test.ts @@ -238,10 +238,11 @@ describe('WorkflowSettingsVue', () => { describe('Custom telemetry tags', () => { beforeEach(() => { settingsStore.settings.activeModules = ['dynamic-credentials', 'otel']; + settingsStore.settings.enterprise.otelCustomSpanAttributes = true; settingsStore.moduleSettings = { otel: { enabled: true } }; }); - it('should show custom telemetry tag settings when OTel is enabled', async () => { + it('should show custom telemetry tag settings when OTel custom span attributes are enabled', async () => { const { getByTestId } = createComponentWithCustomTelemetryTagsStub({ pinia }); await flushPromises(); @@ -258,6 +259,15 @@ describe('WorkflowSettingsVue', () => { expect(queryByTestId('workflow-settings-custom-telemetry-tags')).not.toBeInTheDocument(); }); + it('should hide custom telemetry tag settings when OTel custom span attributes are not licensed', async () => { + settingsStore.settings.enterprise.otelCustomSpanAttributes = false; + const { queryByTestId } = createComponentWithCustomTelemetryTagsStub({ pinia }); + + await flushPromises(); + + expect(queryByTestId('workflow-settings-custom-telemetry-tags')).not.toBeInTheDocument(); + }); + it('should save workflow settings with custom telemetry tags emitted by the child', async () => { const { getByTestId, getByRole } = createComponentWithCustomTelemetryTagsStub({ pinia }); await flushPromises(); diff --git a/packages/frontend/editor-ui/src/app/components/WorkflowSettings/WorkflowSettings.vue b/packages/frontend/editor-ui/src/app/components/WorkflowSettings/WorkflowSettings.vue index adaff023bb3..45800932442 100644 --- a/packages/frontend/editor-ui/src/app/components/WorkflowSettings/WorkflowSettings.vue +++ b/packages/frontend/editor-ui/src/app/components/WorkflowSettings/WorkflowSettings.vue @@ -1708,7 +1708,7 @@ onBeforeUnmount(() => { { }); }); - describe('isOtelEnabled', () => { + describe('isOtelCustomSpanAttributesEnabled', () => { it('should return false when otel module is not active', async () => { getSettings.mockResolvedValueOnce({ ...mockSettings, activeModules: [], + enterprise: { otelCustomSpanAttributes: true }, }); const settingsStore = useSettingsStore(); await settingsStore.getSettings(); settingsStore.moduleSettings = { otel: { enabled: true } }; - expect(settingsStore.isOtelEnabled).toBe(false); + expect(settingsStore.isOtelCustomSpanAttributesEnabled).toBe(false); }); it('should return false when otel module is active but not enabled in moduleSettings', async () => { getSettings.mockResolvedValueOnce({ ...mockSettings, activeModules: ['otel'], + enterprise: { otelCustomSpanAttributes: true }, }); const settingsStore = useSettingsStore(); await settingsStore.getSettings(); settingsStore.moduleSettings = { otel: { enabled: false } }; - expect(settingsStore.isOtelEnabled).toBe(false); + expect(settingsStore.isOtelCustomSpanAttributesEnabled).toBe(false); }); - it('should return true when otel module is active and enabled', async () => { + it('should return false when otel module is active and enabled but not licensed', async () => { getSettings.mockResolvedValueOnce({ ...mockSettings, activeModules: ['otel'], + enterprise: { otelCustomSpanAttributes: false }, }); const settingsStore = useSettingsStore(); await settingsStore.getSettings(); settingsStore.moduleSettings = { otel: { enabled: true } }; - expect(settingsStore.isOtelEnabled).toBe(true); + expect(settingsStore.isOtelCustomSpanAttributesEnabled).toBe(false); + }); + + it('should return true when otel module is active, enabled, and licensed', async () => { + getSettings.mockResolvedValueOnce({ + ...mockSettings, + activeModules: ['otel'], + enterprise: { otelCustomSpanAttributes: true }, + }); + + const settingsStore = useSettingsStore(); + await settingsStore.getSettings(); + settingsStore.moduleSettings = { otel: { enabled: true } }; + + expect(settingsStore.isOtelCustomSpanAttributesEnabled).toBe(true); }); }); }); diff --git a/packages/frontend/editor-ui/src/app/stores/settings.store.ts b/packages/frontend/editor-ui/src/app/stores/settings.store.ts index 61fed174e62..a5e74ccc00b 100644 --- a/packages/frontend/editor-ui/src/app/stores/settings.store.ts +++ b/packages/frontend/editor-ui/src/app/stores/settings.store.ts @@ -170,9 +170,14 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => { () => isModuleActive('chat-hub') && moduleSettings.value['chat-hub']?.enabled !== false, ); - const isOtelEnabled = computed( - () => isModuleActive('otel') === true && moduleSettings.value.otel?.enabled === true, - ); + const isOtelCustomSpanAttributesEnabled = computed(() => { + const isOtelCustomSpanAttributesLicensed = + settings.value.enterprise?.otelCustomSpanAttributes === true; + const isOtelModuleActive = + isModuleActive('otel') === true && moduleSettings.value.otel?.enabled === true; + + return isOtelCustomSpanAttributesLicensed && isOtelModuleActive; + }); // Opt-in flag: the `node-tools-searcher` token must be listed in the backend // `N8N_AGENTS_MODULES` env var for this to evaluate true. @@ -477,7 +482,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => { isAgentModuleActive, isDataTableFeatureEnabled, isChatFeatureEnabled, - isOtelEnabled, + isOtelCustomSpanAttributesEnabled, isAgentsNodeToolsFeatureEnabled, isAgentsKnowledgeBaseFeatureEnabled, isPublicChatTriggerDisabled, diff --git a/packages/frontend/editor-ui/src/features/collaboration/projects/views/ProjectSettings.test.ts b/packages/frontend/editor-ui/src/features/collaboration/projects/views/ProjectSettings.test.ts index 990ccab5577..4240ba1ab6a 100644 --- a/packages/frontend/editor-ui/src/features/collaboration/projects/views/ProjectSettings.test.ts +++ b/packages/frontend/editor-ui/src/features/collaboration/projects/views/ProjectSettings.test.ts @@ -743,12 +743,12 @@ describe('ProjectSettings', () => { describe('Custom telemetry tags', () => { beforeEach(() => { - settingsStore.isOtelEnabled = true; + settingsStore.isOtelCustomSpanAttributesEnabled = true; projectsStore.updateProject.mockResolvedValue(undefined); }); it('should not render telemetry tags section when OTel is disabled', () => { - settingsStore.isOtelEnabled = false; + settingsStore.isOtelCustomSpanAttributesEnabled = false; const { queryByTestId } = renderComponent(); expect(queryByTestId('project-telemetry-tag-add')).not.toBeInTheDocument(); }); @@ -843,7 +843,7 @@ describe('ProjectSettings', () => { }); it('should not include customTelemetryTags in payload when OTel is disabled', async () => { - settingsStore.isOtelEnabled = false; + settingsStore.isOtelCustomSpanAttributesEnabled = false; const updateSpy = vi.spyOn(projectsStore, 'updateProject').mockResolvedValue(undefined); const { getByTestId } = renderComponent(); diff --git a/packages/frontend/editor-ui/src/features/collaboration/projects/views/ProjectSettings.vue b/packages/frontend/editor-ui/src/features/collaboration/projects/views/ProjectSettings.vue index cb9cb32c805..558b7b37fbf 100644 --- a/packages/frontend/editor-ui/src/features/collaboration/projects/views/ProjectSettings.vue +++ b/packages/frontend/editor-ui/src/features/collaboration/projects/views/ProjectSettings.vue @@ -358,7 +358,7 @@ const updateProject = async () => { await projectsStore.updateProject(projectsStore.currentProject.id, { name: formData.value.name ?? '', description: formData.value.description ?? '', - ...(settingsStore.isOtelEnabled + ...(settingsStore.isOtelCustomSpanAttributesEnabled ? { customTelemetryTags: formData.value.customTelemetryTags } : {}), }); @@ -729,7 +729,7 @@ onMounted(async () => { /> -
+

diff --git a/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialSharing.ee.test.ts b/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialSharing.ee.test.ts index d41fd7110ac..4bb5f09b7b6 100644 --- a/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialSharing.ee.test.ts +++ b/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialSharing.ee.test.ts @@ -146,6 +146,7 @@ describe('CredentialSharing.ee', () => { customRoles: false, personalSpacePolicy: false, dataRedaction: false, + otelCustomSpanAttributes: false, }); }); diff --git a/packages/frontend/editor-ui/src/features/ndv/settings/components/NodeSettings.vue b/packages/frontend/editor-ui/src/features/ndv/settings/components/NodeSettings.vue index 77a11de1af2..05f50425b1b 100644 --- a/packages/frontend/editor-ui/src/features/ndv/settings/components/NodeSettings.vue +++ b/packages/frontend/editor-ui/src/features/ndv/settings/components/NodeSettings.vue @@ -503,7 +503,7 @@ const nodeSettings = computed(() => createCommonNodeSettings( isToolNode.value || isModelNode.value, i18n.baseText.bind(i18n), - settingsStore.isOtelEnabled, + settingsStore.isOtelCustomSpanAttributesEnabled, ), ); diff --git a/packages/frontend/editor-ui/src/features/ndv/shared/ndv.utils.test.ts b/packages/frontend/editor-ui/src/features/ndv/shared/ndv.utils.test.ts index 84fb4e3c75d..dbd12f864c1 100644 --- a/packages/frontend/editor-ui/src/features/ndv/shared/ndv.utils.test.ts +++ b/packages/frontend/editor-ui/src/features/ndv/shared/ndv.utils.test.ts @@ -597,7 +597,7 @@ describe('createCommonNodeSettings', () => { } }); - it('should not include customTelemetryTags when isOtelEnabled is false', () => { + it('should not include customTelemetryTags when canUseOtelCustomSpanAttributes is false', () => { const regularSettings = createCommonNodeSettings(false, mockT, false); const toolSettings = createCommonNodeSettings(true, mockT, false); @@ -605,12 +605,12 @@ describe('createCommonNodeSettings', () => { expect(toolSettings.map((s) => s.name)).not.toContain('customTelemetryTags'); }); - it('should not include customTelemetryTags when isOtelEnabled is omitted', () => { + it('should not include customTelemetryTags when canUseOtelCustomSpanAttributes is omitted', () => { const settings = createCommonNodeSettings(false, mockT); expect(settings.map((s) => s.name)).not.toContain('customTelemetryTags'); }); - it('should include customTelemetryTags as the last setting when isOtelEnabled is true', () => { + it('should include customTelemetryTags as the last setting when canUseOtelCustomSpanAttributes is true', () => { const regularSettings = createCommonNodeSettings(false, mockT, true); const toolSettings = createCommonNodeSettings(true, mockT, true); diff --git a/packages/frontend/editor-ui/src/features/ndv/shared/ndv.utils.ts b/packages/frontend/editor-ui/src/features/ndv/shared/ndv.utils.ts index b3cb1772743..3e7fa58b00b 100644 --- a/packages/frontend/editor-ui/src/features/ndv/shared/ndv.utils.ts +++ b/packages/frontend/editor-ui/src/features/ndv/shared/ndv.utils.ts @@ -458,7 +458,7 @@ export function shouldSkipParamValidation( export function createCommonNodeSettings( isToolOrModelNode: boolean, t: (key: BaseTextKey) => string, - isOtelEnabled = false, + canUseOtelCustomSpanAttributes = false, ) { const ret: INodeProperties[] = []; @@ -580,7 +580,7 @@ export function createCommonNodeSettings( }, ); - if (isOtelEnabled) { + if (canUseOtelCustomSpanAttributes) { ret.push({ displayName: t('nodeSettings.customTelemetryTags.displayName'), name: 'customTelemetryTags', diff --git a/packages/frontend/editor-ui/src/features/shared/toolConfig/NodeToolSettingsContent.vue b/packages/frontend/editor-ui/src/features/shared/toolConfig/NodeToolSettingsContent.vue index 5a0632136be..21cd33d167f 100644 --- a/packages/frontend/editor-ui/src/features/shared/toolConfig/NodeToolSettingsContent.vue +++ b/packages/frontend/editor-ui/src/features/shared/toolConfig/NodeToolSettingsContent.vue @@ -94,9 +94,11 @@ const tabOptions = computed>>(() => { }); const nodeSettings = computed(() => - createCommonNodeSettings(true, i18n.baseText.bind(i18n), settingsStore.isOtelEnabled).filter( - (s) => s.name !== 'notes' && s.name !== 'notesInFlow', - ), + createCommonNodeSettings( + true, + i18n.baseText.bind(i18n), + settingsStore.isOtelCustomSpanAttributesEnabled, + ).filter((s) => s.name !== 'notes' && s.name !== 'notesInFlow'), ); const settingsNodeValues = computed(() => { diff --git a/packages/frontend/editor-ui/src/features/shared/toolConfig/__tests__/NodeToolSettingsContent.test.ts b/packages/frontend/editor-ui/src/features/shared/toolConfig/__tests__/NodeToolSettingsContent.test.ts index 8fd425df006..fc75d0f69de 100644 --- a/packages/frontend/editor-ui/src/features/shared/toolConfig/__tests__/NodeToolSettingsContent.test.ts +++ b/packages/frontend/editor-ui/src/features/shared/toolConfig/__tests__/NodeToolSettingsContent.test.ts @@ -425,8 +425,8 @@ describe('NodeToolSettingsContent', () => { }); describe('customTelemetryTags', () => { - it('should show settings tab when isOtelEnabled is true', () => { - settingsStore.isOtelEnabled = true; + it('should show settings tab when canUseOtelCustomSpanAttributes is true', () => { + settingsStore.isOtelCustomSpanAttributesEnabled = true; const { getByText } = renderComponent({ props: { initialNode: createMockNode() }, @@ -435,8 +435,8 @@ describe('NodeToolSettingsContent', () => { expect(getByText('nodeSettings.settings')).toBeTruthy(); }); - it('should not show settings tab from otel alone when isOtelEnabled is false', () => { - settingsStore.isOtelEnabled = false; + it('should not show settings tab from otel alone when canUseOtelCustomSpanAttributes is false', () => { + settingsStore.isOtelCustomSpanAttributesEnabled = false; const { queryByText } = renderComponent({ props: { initialNode: createMockNode() },