diff --git a/cypress/e2e/42-nps-survey.cy.ts b/cypress/e2e/42-nps-survey.cy.ts index 11e5ebb88e..aeddc9452d 100644 --- a/cypress/e2e/42-nps-survey.cy.ts +++ b/cypress/e2e/42-nps-survey.cy.ts @@ -31,6 +31,8 @@ describe('NpsSurvey', () => { config: { key: 'test', url: 'https://telemetry-test.n8n.io', + proxy: 'http://localhost:5678/rest/telemetry/proxy', + sourceConfig: 'http://localhost:5678/rest/telemetry/rudderstack', }, }; } @@ -77,6 +79,8 @@ describe('NpsSurvey', () => { config: { key: 'test', url: 'https://telemetry-test.n8n.io', + proxy: 'http://localhost:5678/rest/telemetry/proxy', + sourceConfig: 'http://localhost:5678/rest/telemetry/rudderstack', }, }; } diff --git a/packages/@n8n/api-types/src/frontend-settings.ts b/packages/@n8n/api-types/src/frontend-settings.ts index 2d4adf9cdd..500168ba24 100644 --- a/packages/@n8n/api-types/src/frontend-settings.ts +++ b/packages/@n8n/api-types/src/frontend-settings.ts @@ -13,6 +13,8 @@ export interface IVersionNotificationSettings { export interface ITelemetryClientConfig { url: string; key: string; + proxy: string; + sourceConfig: string; } export interface ITelemetrySettings { diff --git a/packages/cli/package.json b/packages/cli/package.json index 3067640a1e..902caebbd3 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -130,6 +130,7 @@ "formidable": "3.5.4", "handlebars": "4.7.8", "helmet": "8.1.0", + "http-proxy-middleware": "^3.0.5", "infisical-node": "1.3.0", "ioredis": "5.3.2", "isbot": "3.6.13", diff --git a/packages/cli/src/controllers/telemetry.controller.ts b/packages/cli/src/controllers/telemetry.controller.ts new file mode 100644 index 0000000000..178d4990d8 --- /dev/null +++ b/packages/cli/src/controllers/telemetry.controller.ts @@ -0,0 +1,59 @@ +import { GlobalConfig } from '@n8n/config'; +import { AuthenticatedRequest } from '@n8n/db'; +import { Get, Post, RestController } from '@n8n/decorators'; +import { NextFunction, Response } from 'express'; +import { createProxyMiddleware, fixRequestBody } from 'http-proxy-middleware'; + +@RestController('/telemetry') +export class TelemetryController { + proxy; + + constructor(private readonly globalConfig: GlobalConfig) { + this.proxy = createProxyMiddleware({ + target: this.globalConfig.diagnostics.frontendConfig.split(';')[1], + changeOrigin: true, + pathRewrite: { + '^/proxy/': '/', // /proxy/v1/track -> /v1/track + }, + on: { + proxyReq: (proxyReq, req) => { + proxyReq.removeHeader('cookie'); + fixRequestBody(proxyReq, req); + return; + }, + }, + }); + } + + @Post('/proxy/:version/track', { skipAuth: true, rateLimit: { limit: 100, windowMs: 60_000 } }) + async track(req: AuthenticatedRequest, res: Response, next: NextFunction) { + await this.proxy(req, res, next); + } + + @Post('/proxy/:version/identify', { skipAuth: true, rateLimit: true }) + async identify(req: AuthenticatedRequest, res: Response, next: NextFunction) { + await this.proxy(req, res, next); + } + + @Post('/proxy/:version/page', { skipAuth: true, rateLimit: { limit: 50, windowMs: 60_000 } }) + async page(req: AuthenticatedRequest, res: Response, next: NextFunction) { + await this.proxy(req, res, next); + } + @Get('/rudderstack/sourceConfig', { skipAuth: true, rateLimit: { limit: 50, windowMs: 60_000 } }) + async sourceConfig() { + const response = await fetch('https://api-rs.n8n.io/sourceConfig', { + headers: { + authorization: + 'Basic ' + btoa(`${this.globalConfig.diagnostics.frontendConfig.split(';')[0]}:`), + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch source config: ${response.statusText}`); + } + + const config: unknown = await response.json(); + + return config; + } +} diff --git a/packages/cli/src/server.ts b/packages/cli/src/server.ts index bd6a10fc1c..2a758aac49 100644 --- a/packages/cli/src/server.ts +++ b/packages/cli/src/server.ts @@ -151,6 +151,10 @@ export class Server extends AbstractServer { this.logger.warn(`SAML initialization failed: ${(error as Error).message}`); } + if (this.globalConfig.diagnostics.enabled) { + await import('@/controllers/telemetry.controller'); + } + // ---------------------------------------- // OIDC // ---------------------------------------- diff --git a/packages/cli/src/services/frontend.service.ts b/packages/cli/src/services/frontend.service.ts index 40acd807cd..fd2223b10a 100644 --- a/packages/cli/src/services/frontend.service.ts +++ b/packages/cli/src/services/frontend.service.ts @@ -89,13 +89,15 @@ export class FrontendService { if (telemetrySettings.enabled) { const conf = this.globalConfig.diagnostics.frontendConfig; const [key, url] = conf.split(';'); + const proxy = `${instanceBaseUrl}/${restEndpoint}/telemetry/proxy`; + const sourceConfig = `${instanceBaseUrl}/${restEndpoint}/telemetry/rudderstack`; if (!key || !url) { this.logger.warn('Diagnostics frontend config is invalid'); telemetrySettings.enabled = false; } - telemetrySettings.config = { key, url }; + telemetrySettings.config = { key, url, proxy, sourceConfig }; } this.settings = { diff --git a/packages/frontend/editor-ui/src/plugins/telemetry.test.ts b/packages/frontend/editor-ui/src/plugins/telemetry.test.ts index 5b2d0c4f7c..42dd60fca9 100644 --- a/packages/frontend/editor-ui/src/plugins/telemetry.test.ts +++ b/packages/frontend/editor-ui/src/plugins/telemetry.test.ts @@ -16,7 +16,7 @@ describe('telemetry', () => { setActivePinia(createPinia()); settingsStore = useSettingsStore(); telemetry.init( - { enabled: true, config: { url: '', key: '' } }, + { enabled: true, config: { proxy: '', key: '', sourceConfig: '', url: '' } }, { versionCli: '1', instanceId: '1' }, ); }); diff --git a/packages/frontend/editor-ui/src/plugins/telemetry/index.ts b/packages/frontend/editor-ui/src/plugins/telemetry/index.ts index bc3144b658..3edddf5fdc 100644 --- a/packages/frontend/editor-ui/src/plugins/telemetry/index.ts +++ b/packages/frontend/editor-ui/src/plugins/telemetry/index.ts @@ -47,7 +47,7 @@ export class Telemetry { if (!telemetrySettings.enabled || !telemetrySettings.config || this.rudderStack) return; const { - config: { key, url }, + config: { key, proxy, sourceConfig }, } = telemetrySettings; const settingsStore = useSettingsStore(); @@ -57,10 +57,10 @@ export class Telemetry { const logging = logLevel === 'debug' ? { logLevel: 'DEBUG' } : {}; - this.initRudderStack(key, url, { + this.initRudderStack(key, proxy, { integrations: { All: false }, loadIntegration: false, - configUrl: 'https://api-rs.n8n.io', + configUrl: sourceConfig, ...logging, }); @@ -201,7 +201,7 @@ export class Telemetry { } } - private initRudderStack(key: string, url: string, options: IDataObject) { + private initRudderStack(key: string, proxy: string, options: IDataObject) { window.rudderanalytics = window.rudderanalytics || []; if (!this.rudderStack) { return; @@ -252,7 +252,7 @@ export class Telemetry { }; this.rudderStack.loadJS(); - this.rudderStack.load(key, url, options); + this.rudderStack.load(key, proxy, options); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f2cf13768d..0305da6edc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1429,6 +1429,9 @@ importers: helmet: specifier: 8.1.0 version: 8.1.0 + http-proxy-middleware: + specifier: ^3.0.5 + version: 3.0.5 infisical-node: specifier: 1.3.0 version: 1.3.0 @@ -7089,6 +7092,9 @@ packages: '@types/html-to-text@9.0.4': resolution: {integrity: sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==} + '@types/http-proxy@1.17.16': + resolution: {integrity: sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==} + '@types/humanize-duration@3.27.1': resolution: {integrity: sha512-K3e+NZlpCKd6Bd/EIdqjFJRFHbrq5TzPPLwREk5Iv/YoIjQrs6ljdAUCo+Lb2xFlGNOjGSE0dqsVD19cZL137w==} @@ -10849,6 +10855,14 @@ packages: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} + http-proxy-middleware@3.0.5: + resolution: {integrity: sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + http-signature@1.4.0: resolution: {integrity: sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==} engines: {node: '>=0.10'} @@ -21621,6 +21635,10 @@ snapshots: '@types/html-to-text@9.0.4': {} + '@types/http-proxy@1.17.16': + dependencies: + '@types/node': 20.19.1 + '@types/humanize-duration@3.27.1': {} '@types/imap@0.8.40': @@ -26311,6 +26329,25 @@ snapshots: transitivePeerDependencies: - supports-color + http-proxy-middleware@3.0.5: + dependencies: + '@types/http-proxy': 1.17.16 + debug: 4.4.1(supports-color@8.1.1) + http-proxy: 1.18.1(debug@4.4.1) + is-glob: 4.0.3 + is-plain-object: 5.0.0 + micromatch: 4.0.8 + transitivePeerDependencies: + - supports-color + + http-proxy@1.18.1(debug@4.4.1): + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.15.9(debug@4.4.1) + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + http-signature@1.4.0: dependencies: assert-plus: 1.0.0