feat: Proxy all RudderStack frontend telemetry events through the backend (#17177)

Co-authored-by: Nikhil Kuriakose <nikhil.kuriakose@n8n.io>
This commit is contained in:
Ricardo Espinoza
2025-07-23 09:17:01 -04:00
committed by GitHub
parent e317c92916
commit 5524b2137a
9 changed files with 116 additions and 7 deletions

View File

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

View File

@@ -13,6 +13,8 @@ export interface IVersionNotificationSettings {
export interface ITelemetryClientConfig {
url: string;
key: string;
proxy: string;
sourceConfig: string;
}
export interface ITelemetrySettings {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

37
pnpm-lock.yaml generated
View File

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