feat: Gate custom telemetry attributes by license (#31723)

This commit is contained in:
Irénée 2026-06-04 12:54:02 +01:00 committed by GitHub
parent f16befcb3b
commit b66d33c305
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 211 additions and 50 deletions

View File

@ -66,6 +66,7 @@ export interface IEnterpriseSettings {
customRoles: boolean;
personalSpacePolicy: boolean;
dataRedaction: boolean;
otelCustomSpanAttributes: boolean;
}
export interface FrontendSettings {

View File

@ -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
// --------------------

View File

@ -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 = {

View File

@ -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<NumericLicenseFeature, number> = {

View File

@ -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<OwnershipService>();
const logger = mock<Logger>();
const licenseState = mock<LicenseState>();
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<OwnershipService>();
const logger = mock<Logger>();
const licenseState = mock<LicenseState>();
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<OwnershipService>();
const logger = mock<Logger>();
const licenseState = mock<LicenseState>();
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<OwnershipService>();
const logger = mock<Logger>();
const licenseState = mock<LicenseState>();
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<NodeExecuteAfterContext['taskData']>),
);
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<OwnershipService>();
const logger = mock<Logger>();
const licenseState = mock<LicenseState>();
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 () => {

View File

@ -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',

View File

@ -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<string, string> | undefined {
if (!tags?.length) return undefined;
const attrs: Record<string, string> = {};
for (const { key, value } of tags) {
attrs[key] = value;
private buildProjectCustomAttributes(
tags: Array<{ key: string; value: string }> | undefined,
): Record<string, string> | undefined {
if (!this.areCustomSpanAttributesLicensed()) return undefined;
if (!tags?.length) return undefined;
const attrs: Record<string, string> = {};
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 {

View File

@ -158,6 +158,7 @@ describe('FrontendService', () => {
const licenseState = mock<LicenseState>({
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', () => {

View File

@ -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()) {

View File

@ -55,6 +55,7 @@ export const defaultSettings: FrontendSettings = {
customRoles: false,
personalSpacePolicy: false,
dataRedaction: false,
otelCustomSpanAttributes: false,
},
executionMode: 'regular',
isMultiMain: false,

View File

@ -294,6 +294,7 @@ export function createMockEnterpriseSettings(
customRoles: false,
personalSpacePolicy: false,
dataRedaction: false,
otelCustomSpanAttributes: false,
...overrides, // Override with any passed properties
};
}

View File

@ -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();

View File

@ -1708,7 +1708,7 @@ onBeforeUnmount(() => {
</ElCol>
</ElRow>
<WorkflowCustomTelemetryTags
v-if="settingsStore.isOtelEnabled"
v-if="settingsStore.isOtelCustomSpanAttributesEnabled"
v-model="workflowSettings.customTelemetryTags"
:is-read-only="isWorkflowSettingsReadOnly"
:save-tags="saveCustomTelemetryTags"

View File

@ -239,44 +239,61 @@ describe('settings.store', () => {
});
});
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);
});
});
});

View File

@ -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,

View File

@ -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();

View File

@ -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 () => {
/>
</div>
</fieldset>
<fieldset v-if="settingsStore.isOtelEnabled">
<fieldset v-if="settingsStore.isOtelCustomSpanAttributesEnabled">
<h3>
<label>{{ i18n.baseText('projects.settings.telemetryTags.label') }}</label>
</h3>

View File

@ -146,6 +146,7 @@ describe('CredentialSharing.ee', () => {
customRoles: false,
personalSpacePolicy: false,
dataRedaction: false,
otelCustomSpanAttributes: false,
});
});

View File

@ -503,7 +503,7 @@ const nodeSettings = computed(() =>
createCommonNodeSettings(
isToolNode.value || isModelNode.value,
i18n.baseText.bind(i18n),
settingsStore.isOtelEnabled,
settingsStore.isOtelCustomSpanAttributesEnabled,
),
);

View File

@ -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);

View File

@ -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',

View File

@ -94,9 +94,11 @@ const tabOptions = computed<Array<ITab<ToolSettingsTab>>>(() => {
});
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<INodeParameters>(() => {

View File

@ -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() },