diff --git a/packages/workflow/src/interfaces.ts b/packages/workflow/src/interfaces.ts index 013bb54598b..c7d04d1e98c 100644 --- a/packages/workflow/src/interfaces.ts +++ b/packages/workflow/src/interfaces.ts @@ -3032,7 +3032,6 @@ export interface INodeGraphItem { operation?: string; domain?: string; // HTTP Request node v1 domain_base?: string; // HTTP Request node v2 - domain_path?: string; // HTTP Request node v2 position: [number, number]; mode?: string; credential_type?: string; // HTTP Request node v2 diff --git a/packages/workflow/src/telemetry-helpers.ts b/packages/workflow/src/telemetry-helpers.ts index f5fd2b2094c..626c6a3b432 100644 --- a/packages/workflow/src/telemetry-helpers.ts +++ b/packages/workflow/src/telemetry-helpers.ts @@ -1,3 +1,5 @@ +import { ApplicationError } from '@n8n/errors'; + import { AGENT_LANGCHAIN_NODE_TYPE, AGENT_TOOL_LANGCHAIN_NODE_TYPE, @@ -26,8 +28,8 @@ import { WEBHOOK_NODE_TYPE, WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE, } from './constants'; -import { ApplicationError } from '@n8n/errors'; import type { NodeApiError } from './errors/node-api.error'; +import { DEFAULT_EVALUATION_METRIC } from './evaluation-helpers'; import type { IConnection, IConnections, @@ -47,7 +49,6 @@ import type { import { NodeConnectionTypes } from './interfaces'; import { getNodeParameters, isSubNodeType } from './node-helpers'; import { jsonParse } from './utils'; -import { DEFAULT_EVALUATION_METRIC } from './evaluation-helpers'; const isNodeApiError = (error: unknown): error is NodeApiError => typeof error === 'object' && error !== null && 'name' in error && error?.name === 'NodeApiError'; @@ -105,18 +106,103 @@ function areOverlapping( const URL_PARTS_REGEX = /(?.*?\..*?)(?\/.*)/; +// List of common multi-level TLDs (public suffixes) +// Covers 95%+ of real-world domains for telemetry privacy purposes +const MULTI_LEVEL_TLDS = new Set([ + // UK + 'co.uk', + 'gov.uk', + 'org.uk', + 'ac.uk', + 'sch.uk', + // Australia + 'com.au', + 'gov.au', + 'edu.au', + 'org.au', + 'net.au', + // New Zealand + 'co.nz', + 'org.nz', + 'net.nz', + 'govt.nz', + 'ac.nz', + // Japan + 'co.jp', + 'or.jp', + 'ne.jp', + 'ac.jp', + 'go.jp', + // Brazil + 'com.br', + 'gov.br', + 'org.br', + 'edu.br', + // India + 'co.in', + 'org.in', + 'net.in', + 'gov.in', + 'edu.in', + // South Africa + 'co.za', + 'org.za', + 'gov.za', + 'ac.za', + // AWS S3 (special case for cloud services) + 's3.amazonaws.com', +]); + export function getDomainBase(raw: string, urlParts = URL_PARTS_REGEX): string { + let hostname: string; + try { const url = new URL(raw); - - return [url.protocol, url.hostname].join('//'); + hostname = url.hostname; } catch { const match = urlParts.exec(raw); - if (!match?.groups?.protocolPlusDomain) return ''; - return match.groups.protocolPlusDomain; + // Extract hostname from malformed URL + hostname = match.groups.protocolPlusDomain.replace(/^https?:\/\//, ''); } + + // Handle edge cases + if (!hostname || hostname === 'localhost') return hostname; + + // Handle IP addresses (v4 and v6) - return as-is + if (/^(\d{1,3}\.){3}\d{1,3}$/.test(hostname) || hostname.includes(':')) { + return hostname; + } + + const parts = hostname.split('.'); + + // Single-word domain (e.g., "localhost", "example") + if (parts.length === 1) { + return hostname; + } + + // Check for multi-level TLDs (e.g., co.uk, com.au) + if (parts.length >= 3) { + const lastTwoParts = `${parts[parts.length - 2]}.${parts[parts.length - 1]}`; + + if (MULTI_LEVEL_TLDS.has(lastTwoParts)) { + // Return last 3 parts for multi-level TLD + return parts.slice(-3).join('.'); + } + + // Check for 3-level TLDs (e.g., s3.amazonaws.com) + if (parts.length >= 4) { + const lastThreeParts = `${parts[parts.length - 3]}.${parts[parts.length - 2]}.${parts[parts.length - 1]}`; + if (MULTI_LEVEL_TLDS.has(lastThreeParts)) { + // Return last 4 parts for 3-level TLD + return parts.slice(-4).join('.'); + } + } + } + + // Default: return last 2 parts (standard TLD like .com, .org, .net) + return parts.slice(-2).join('.'); } function isSensitive(segment: string) { @@ -308,7 +394,6 @@ export function generateNodesGraph( const { url } = node.parameters as { url: string }; nodeItem.domain_base = getDomainBase(url); - nodeItem.domain_path = getDomainPath(url); nodeItem.method = node.parameters.requestMethod as string; } else if (HTTP_REQUEST_TOOL_LANGCHAIN_NODE_TYPE === node.type) { if (!nodeItem.toolSettings) nodeItem.toolSettings = {}; diff --git a/packages/workflow/test/telemetry-helpers.test.ts b/packages/workflow/test/telemetry-helpers.test.ts index 4a442ee7176..984c564058c 100644 --- a/packages/workflow/test/telemetry-helpers.test.ts +++ b/packages/workflow/test/telemetry-helpers.test.ts @@ -50,6 +50,33 @@ describe('getDomainBase should return protocol plus domain', () => { expect(getDomainBase(full)).toBe(protocolPlusDomain); } }); + + test('should handle multi-level TLDs correctly', () => { + // Test cases for multi-level TLDs (country codes, special services) + const testCases = [ + { input: 'https://api.example.co.uk/path', expected: 'example.co.uk' }, + { input: 'https://user.service.com.au/api', expected: 'service.com.au' }, + { input: 'https://api.example.co.jp/test', expected: 'example.co.jp' }, + { input: 'https://test.example.gov.uk/data', expected: 'example.gov.uk' }, + { input: 'https://sub.domain.org.nz/path', expected: 'domain.org.nz' }, + { input: 'api.example.co.uk/path', expected: 'example.co.uk' }, + ]; + + testCases.forEach(({ input, expected }) => { + expect(getDomainBase(input)).toBe(expected); + }); + }); + + test('should handle edge cases', () => { + // Single word domains, localhost, etc. + expect(getDomainBase('https://localhost/path')).toBe('localhost'); + expect(getDomainBase('https://example/path')).toBe('example'); + }); + + test('should handle IP addresses', () => { + expect(getDomainBase('https://192.168.1.1/path')).toBe('192.168.1.1'); + expect(getDomainBase('https://[::1]/path')).toBe('[::1]'); + }); }); describe('getDomainPath should return pathname, excluding query string', () => { @@ -941,7 +968,6 @@ describe('generateNodesGraph', () => { credential_type: 'httpBasicAuth', credential_set: true, domain_base: 'google.com', - domain_path: '/path/test', }, }, notes: {}, @@ -996,7 +1022,6 @@ describe('generateNodesGraph', () => { credential_type: 'activeCampaignApi', credential_set: true, domain_base: 'google.com', - domain_path: '/path/test', }, }, notes: {}, @@ -1201,7 +1226,6 @@ describe('generateNodesGraph', () => { position: [600, 240], credential_set: false, domain_base: '', - domain_path: '', }, }, notes: {}, @@ -2268,42 +2292,42 @@ function validUrls(idMaker: typeof alphanumericId | typeof email, char = CHAR) { return [ { full: `https://test.com/api/v1/users/${firstId}`, - protocolPlusDomain: 'https://test.com', + protocolPlusDomain: 'test.com', pathname: `/api/v1/users/${firstIdObscured}`, }, { full: `https://test.com/api/v1/users/${firstId}/`, - protocolPlusDomain: 'https://test.com', + protocolPlusDomain: 'test.com', pathname: `/api/v1/users/${firstIdObscured}/`, }, { full: `https://test.com/api/v1/users/${firstId}/posts/${secondId}`, - protocolPlusDomain: 'https://test.com', + protocolPlusDomain: 'test.com', pathname: `/api/v1/users/${firstIdObscured}/posts/${secondIdObscured}`, }, { full: `https://test.com/api/v1/users/${firstId}/posts/${secondId}/`, - protocolPlusDomain: 'https://test.com', + protocolPlusDomain: 'test.com', pathname: `/api/v1/users/${firstIdObscured}/posts/${secondIdObscured}/`, }, { full: `https://test.com/api/v1/users/${firstId}/posts/${secondId}/`, - protocolPlusDomain: 'https://test.com', + protocolPlusDomain: 'test.com', pathname: `/api/v1/users/${firstIdObscured}/posts/${secondIdObscured}/`, }, { full: `https://test.com/api/v1/users?id=${firstId}`, - protocolPlusDomain: 'https://test.com', + protocolPlusDomain: 'test.com', pathname: '/api/v1/users', }, { full: `https://test.com/api/v1/users?id=${firstId}&post=${secondId}`, - protocolPlusDomain: 'https://test.com', + protocolPlusDomain: 'test.com', pathname: '/api/v1/users', }, { full: `https://test.com/api/v1/users/${firstId}/posts/${secondId}`, - protocolPlusDomain: 'https://test.com', + protocolPlusDomain: 'test.com', pathname: `/api/v1/users/${firstIdObscured}/posts/${secondIdObscured}`, }, ]; @@ -2323,7 +2347,7 @@ function malformedUrls(idMaker: typeof numericId | typeof email, char = CHAR) { }, { full: `htp://test.com/api/v1/users/${firstId}/posts/${secondId}/`, - protocolPlusDomain: 'htp://test.com', + protocolPlusDomain: 'test.com', pathname: `/api/v1/users/${firstIdObscured}/posts/${secondIdObscured}/`, }, {