feat: Introduce debug info button (#9895)

This commit is contained in:
Iván Ovejero 2024-07-03 09:38:21 +02:00 committed by GitHub
parent ae67d6b753
commit be9a247577
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 288 additions and 7 deletions

View file

@ -338,6 +338,10 @@ export class License {
); );
} }
getConsumerId() {
return this.manager?.getConsumerId() ?? 'unknown';
}
// Helper functions for computed data // Helper functions for computed data
getUsersLimit() { getUsersLimit() {
return this.getFeatureValue(LICENSE_QUOTAS.USERS_LIMIT) ?? UNLIMITED_LICENSE_QUOTA; return this.getFeatureValue(LICENSE_QUOTAS.USERS_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;

View file

@ -1,3 +1,4 @@
import fs from 'node:fs';
import { Container, Service } from 'typedi'; import { Container, Service } from 'typedi';
import uniq from 'lodash/uniq'; import uniq from 'lodash/uniq';
import { createWriteStream } from 'fs'; import { createWriteStream } from 'fs';
@ -83,6 +84,8 @@ export class FrontendService {
} }
this.settings = { this.settings = {
isDocker: this.isDocker(),
databaseType: config.getEnv('database.type'),
previewMode: process.env.N8N_PREVIEW_MODE === 'true', previewMode: process.env.N8N_PREVIEW_MODE === 'true',
endpointForm: config.getEnv('endpoints.form'), endpointForm: config.getEnv('endpoints.form'),
endpointFormTest: config.getEnv('endpoints.formTest'), endpointFormTest: config.getEnv('endpoints.formTest'),
@ -92,6 +95,7 @@ export class FrontendService {
saveDataErrorExecution: config.getEnv('executions.saveDataOnError'), saveDataErrorExecution: config.getEnv('executions.saveDataOnError'),
saveDataSuccessExecution: config.getEnv('executions.saveDataOnSuccess'), saveDataSuccessExecution: config.getEnv('executions.saveDataOnSuccess'),
saveManualExecutions: config.getEnv('executions.saveDataManualExecutions'), saveManualExecutions: config.getEnv('executions.saveDataManualExecutions'),
saveExecutionProgress: config.getEnv('executions.saveExecutionProgress'),
executionTimeout: config.getEnv('executions.timeout'), executionTimeout: config.getEnv('executions.timeout'),
maxExecutionTimeout: config.getEnv('executions.maxTimeout'), maxExecutionTimeout: config.getEnv('executions.maxTimeout'),
workflowCallerPolicyDefaultOption: config.getEnv('workflows.callerPolicyDefaultOption'), workflowCallerPolicyDefaultOption: config.getEnv('workflows.callerPolicyDefaultOption'),
@ -99,7 +103,9 @@ export class FrontendService {
urlBaseWebhook: this.urlService.getWebhookBaseUrl(), urlBaseWebhook: this.urlService.getWebhookBaseUrl(),
urlBaseEditor: instanceBaseUrl, urlBaseEditor: instanceBaseUrl,
binaryDataMode: config.getEnv('binaryDataManager.mode'), binaryDataMode: config.getEnv('binaryDataManager.mode'),
nodeJsVersion: process.version.replace(/^v/, ''),
versionCli: '', versionCli: '',
concurrency: config.getEnv('executions.concurrency.productionLimit'),
authCookie: { authCookie: {
secure: config.getEnv('secure_cookie'), secure: config.getEnv('secure_cookie'),
}, },
@ -196,6 +202,7 @@ export class FrontendService {
}, },
hideUsagePage: config.getEnv('hideUsagePage'), hideUsagePage: config.getEnv('hideUsagePage'),
license: { license: {
consumerId: 'unknown',
environment: config.getEnv('license.tenantId') === 1 ? 'production' : 'staging', environment: config.getEnv('license.tenantId') === 1 ? 'production' : 'staging',
}, },
variables: { variables: {
@ -218,6 +225,14 @@ export class FrontendService {
pruneTime: -1, pruneTime: -1,
licensePruneTime: -1, licensePruneTime: -1,
}, },
pruning: {
isEnabled: config.getEnv('executions.pruneData'),
maxAge: config.getEnv('executions.pruneDataMaxAge'),
maxCount: config.getEnv('executions.pruneDataMaxCount'),
},
security: {
blockFileAccessToN8nFiles: config.getEnv('security.blockFileAccessToN8nFiles'),
},
}; };
} }
@ -269,6 +284,9 @@ export class FrontendService {
const isS3Available = config.getEnv('binaryDataManager.availableModes').includes('s3'); const isS3Available = config.getEnv('binaryDataManager.availableModes').includes('s3');
const isS3Licensed = this.license.isBinaryDataS3Licensed(); const isS3Licensed = this.license.isBinaryDataS3Licensed();
this.settings.license.planName = this.license.getPlanName();
this.settings.license.consumerId = this.license.getConsumerId();
// refresh enterprise status // refresh enterprise status
Object.assign(this.settings.enterprise, { Object.assign(this.settings.enterprise, {
sharing: this.license.isSharingEnabled(), sharing: this.license.isSharingEnabled(),
@ -368,4 +386,20 @@ export class FrontendService {
} }
} }
} }
/**
* Whether this instance is running inside a Docker container.
*
* Based on: https://github.com/sindresorhus/is-docker
*/
private isDocker() {
try {
return (
fs.existsSync('/.dockerenv') ||
fs.readFileSync('/proc/self/cgroup', 'utf8').includes('docker')
);
} catch {
return false;
}
}
} }

View file

@ -1123,7 +1123,7 @@ export interface RootState {
urlBaseEditor: string; urlBaseEditor: string;
instanceId: string; instanceId: string;
isNpmAvailable: boolean; isNpmAvailable: boolean;
binaryDataMode: string; binaryDataMode: 'default' | 'filesystem' | 's3';
} }
export interface NodeMetadataMap { export interface NodeMetadataMap {
@ -1380,9 +1380,10 @@ export interface ISettingsState {
enabled: boolean; enabled: boolean;
}; };
onboardingCallPromptEnabled: boolean; onboardingCallPromptEnabled: boolean;
saveDataErrorExecution: string; saveDataErrorExecution: WorkflowSettings.SaveDataExecution;
saveDataSuccessExecution: string; saveDataSuccessExecution: WorkflowSettings.SaveDataExecution;
saveManualExecutions: boolean; saveManualExecutions: boolean;
saveDataProgressExecution: boolean;
} }
export type NodeTypesByTypeNameAndVersion = { export type NodeTypesByTypeNameAndVersion = {

View file

@ -1,6 +1,13 @@
import type { IN8nUISettings } from 'n8n-workflow'; import type { IN8nUISettings } from 'n8n-workflow';
export const defaultSettings: IN8nUISettings = { export const defaultSettings: IN8nUISettings = {
databaseType: 'sqlite',
isDocker: false,
pruning: {
isEnabled: false,
maxAge: 0,
maxCount: 0,
},
allowedModules: {}, allowedModules: {},
communityNodesEnabled: false, communityNodesEnabled: false,
defaultLocale: '', defaultLocale: '',
@ -40,7 +47,7 @@ export const defaultSettings: IN8nUISettings = {
hiringBannerEnabled: false, hiringBannerEnabled: false,
instanceId: '', instanceId: '',
isNpmAvailable: false, isNpmAvailable: false,
license: { environment: 'development' }, license: { environment: 'development', consumerId: 'unknown' },
logLevel: 'info', logLevel: 'info',
maxExecutionTimeout: 0, maxExecutionTimeout: 0,
oauthCallbackUrls: { oauth1: '', oauth2: '' }, oauthCallbackUrls: { oauth1: '', oauth2: '' },
@ -60,6 +67,7 @@ export const defaultSettings: IN8nUISettings = {
saveDataErrorExecution: 'DEFAULT', saveDataErrorExecution: 'DEFAULT',
saveDataSuccessExecution: 'DEFAULT', saveDataSuccessExecution: 'DEFAULT',
saveManualExecutions: false, saveManualExecutions: false,
saveExecutionProgress: false,
sso: { sso: {
ldap: { loginEnabled: false, loginLabel: '' }, ldap: { loginEnabled: false, loginLabel: '' },
saml: { loginEnabled: false, loginLabel: '' }, saml: { loginEnabled: false, loginLabel: '' },
@ -81,6 +89,8 @@ export const defaultSettings: IN8nUISettings = {
quota: 10, quota: 10,
}, },
versionCli: '', versionCli: '',
nodeJsVersion: '',
concurrency: -1,
versionNotifications: { versionNotifications: {
enabled: true, enabled: true,
endpoint: '', endpoint: '',
@ -113,4 +123,7 @@ export const defaultSettings: IN8nUISettings = {
pruneTime: 0, pruneTime: 0,
licensePruneTime: 0, licensePruneTime: 0,
}, },
security: {
blockFileAccessToN8nFiles: false,
},
}; };

View file

@ -68,6 +68,7 @@ export const SETTINGS_STORE_DEFAULT_STATE: ISettingsState = {
onboardingCallPromptEnabled: false, onboardingCallPromptEnabled: false,
saveDataErrorExecution: 'all', saveDataErrorExecution: 'all',
saveDataSuccessExecution: 'all', saveDataSuccessExecution: 'all',
saveDataProgressExecution: false,
saveManualExecutions: false, saveManualExecutions: false,
}; };

View file

@ -42,6 +42,16 @@
<n8n-text>{{ rootStore.instanceId }}</n8n-text> <n8n-text>{{ rootStore.instanceId }}</n8n-text>
</el-col> </el-col>
</el-row> </el-row>
<el-row>
<el-col :span="8" class="info-name">
<n8n-text>{{ $locale.baseText('about.debug.title') }}</n8n-text>
</el-col>
<el-col :span="16">
<div :class="$style.debugInfo" @click="copyDebugInfoToClipboard">
<n8n-link>{{ $locale.baseText('about.debug.message') }}</n8n-link>
</div>
</el-col>
</el-row>
</div> </div>
</template> </template>
@ -66,6 +76,9 @@ import Modal from './Modal.vue';
import { ABOUT_MODAL_KEY } from '../constants'; import { ABOUT_MODAL_KEY } from '../constants';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useRootStore } from '@/stores/root.store'; import { useRootStore } from '@/stores/root.store';
import { useToast } from '@/composables/useToast';
import { useClipboard } from '@/composables/useClipboard';
import { useDebugInfo } from '@/composables/useDebugInfo';
export default defineComponent({ export default defineComponent({
name: 'About', name: 'About',
@ -85,6 +98,15 @@ export default defineComponent({
closeDialog() { closeDialog() {
this.modalBus.emit('close'); this.modalBus.emit('close');
}, },
async copyDebugInfoToClipboard() {
useToast().showToast({
title: this.$locale.baseText('about.debug.toast.title'),
message: this.$locale.baseText('about.debug.toast.message'),
type: 'info',
duration: 5000,
});
await useClipboard().copy(useDebugInfo().generateDebugInfo());
},
}, },
}); });
</script> </script>

View file

@ -0,0 +1,148 @@
import { useSettingsStore } from '@/stores/settings.store';
import type { WorkflowSettings } from 'n8n-workflow';
type DebugInfo = {
core: {
n8nVersion: string;
platform: 'docker (cloud)' | 'docker (self-hosted)' | 'npm';
nodeJsVersion: string;
database: 'sqlite' | 'mysql' | 'mariadb' | 'postgres';
executionMode: 'regular' | 'scaling';
license: 'community' | 'enterprise (production)' | 'enterprise (sandbox)';
consumerId: string;
concurrency: number;
};
storage: {
success: WorkflowSettings.SaveDataExecution;
error: WorkflowSettings.SaveDataExecution;
progress: boolean;
manual: boolean;
binaryMode: 'memory' | 'filesystem' | 's3';
};
pruning:
| {
enabled: false;
}
| {
enabled: true;
maxAge: `${number} hours`;
maxCount: `${number} executions`;
};
/**
* Reported only if insecure settings are found.
*/
security?: {
secureCookie?: boolean;
blockFileAccessToN8nFiles?: boolean;
};
};
export function useDebugInfo() {
const store = useSettingsStore();
const coreInfo = () => {
return {
n8nVersion: store.versionCli,
platform:
store.isDocker && store.deploymentType === 'cloud'
? 'docker (cloud)'
: store.isDocker
? 'docker (self-hosted)'
: 'npm',
nodeJsVersion: store.nodeJsVersion,
database:
store.databaseType === 'postgresdb'
? 'postgres'
: store.databaseType === 'mysqldb'
? 'mysql'
: store.databaseType,
executionMode: store.isQueueModeEnabled ? 'scaling' : 'regular',
concurrency: store.settings.concurrency,
license:
store.planName === 'Community'
? (store.planName.toLowerCase() as 'community')
: store.settings.license.environment === 'production'
? 'enterprise (production)'
: 'enterprise (sandbox)',
consumerId: store.consumerId,
} as const;
};
const storageInfo = (): DebugInfo['storage'] => {
return {
success: store.saveDataSuccessExecution,
error: store.saveDataErrorExecution,
progress: store.saveDataProgressExecution,
manual: store.saveManualExecutions,
binaryMode: store.binaryDataMode === 'default' ? 'memory' : store.binaryDataMode,
};
};
const pruningInfo = () => {
if (!store.pruning.isEnabled) return { enabled: false } as const;
return {
enabled: true,
maxAge: `${store.pruning.maxAge} hours`,
maxCount: `${store.pruning.maxCount} executions`,
} as const;
};
const securityInfo = () => {
const info: DebugInfo['security'] = {};
if (!store.security.blockFileAccessToN8nFiles) info.blockFileAccessToN8nFiles = false;
if (!store.security.secureCookie) info.secureCookie = false;
if (Object.keys(info).length === 0) return;
return info;
};
const gatherDebugInfo = () => {
const debugInfo: DebugInfo = {
core: coreInfo(),
storage: storageInfo(),
pruning: pruningInfo(),
};
const security = securityInfo();
if (security) debugInfo.security = security;
return debugInfo;
};
const toMarkdown = (debugInfo: DebugInfo): string => {
let markdown = '# Debug info\n\n';
for (const sectionKey in debugInfo) {
markdown += `## ${sectionKey}\n\n`;
const section = debugInfo[sectionKey as keyof DebugInfo];
if (!section) continue;
for (const itemKey in section) {
const itemValue = section[itemKey as keyof typeof section];
markdown += `- ${itemKey}: ${itemValue}\n`;
}
markdown += '\n';
}
return markdown;
};
const appendTimestamp = (markdown: string) => {
return `${markdown}Generated at: ${new Date().toISOString()}`;
};
const generateDebugInfo = () => {
return appendTimestamp(toMarkdown(gatherDebugInfo()));
};
return {
generateDebugInfo,
};
}

View file

@ -78,6 +78,10 @@
"about.n8nVersion": "n8n Version", "about.n8nVersion": "n8n Version",
"about.sourceCode": "Source Code", "about.sourceCode": "Source Code",
"about.instanceID": "Instance ID", "about.instanceID": "Instance ID",
"about.debug.title": "Debug",
"about.debug.message": "Copy debug information",
"about.debug.toast.title": "Debug info",
"about.debug.toast.message": "Copied debug info to clipboard",
"askAi.dialog.title": "'Ask AI' is almost ready", "askAi.dialog.title": "'Ask AI' is almost ready",
"askAi.dialog.body": "Were still applying the finishing touches. Soon, you will be able to <strong>automatically generate code from simple text prompts</strong>. Join the waitlist to get early access to this feature.", "askAi.dialog.body": "Were still applying the finishing touches. Soon, you will be able to <strong>automatically generate code from simple text prompts</strong>. Join the waitlist to get early access to this feature.",
"askAi.dialog.signup": "Join Waitlist", "askAi.dialog.signup": "Join Waitlist",

View file

@ -68,15 +68,50 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
saveDataErrorExecution: 'all', saveDataErrorExecution: 'all',
saveDataSuccessExecution: 'all', saveDataSuccessExecution: 'all',
saveManualExecutions: false, saveManualExecutions: false,
saveDataProgressExecution: false,
}), }),
getters: { getters: {
isDocker(): boolean {
return this.settings.isDocker;
},
databaseType(): 'sqlite' | 'mariadb' | 'mysqldb' | 'postgresdb' {
return this.settings.databaseType;
},
planName(): string {
return this.settings.license.planName ?? 'Community';
},
consumerId(): string {
return this.settings.license.consumerId;
},
binaryDataMode(): 'default' | 'filesystem' | 's3' {
return this.settings.binaryDataMode;
},
pruning(): { isEnabled: boolean; maxAge: number; maxCount: number } {
return this.settings.pruning;
},
security(): {
blockFileAccessToN8nFiles: boolean;
secureCookie: boolean;
} {
return {
blockFileAccessToN8nFiles: this.settings.security.blockFileAccessToN8nFiles,
secureCookie: this.settings.authCookie.secure,
};
},
isEnterpriseFeatureEnabled() { isEnterpriseFeatureEnabled() {
return (feature: EnterpriseEditionFeatureValue): boolean => return (feature: EnterpriseEditionFeatureValue): boolean =>
Boolean(this.settings.enterprise?.[feature]); Boolean(this.settings.enterprise?.[feature]);
}, },
versionCli(): string { versionCli(): string {
return this.settings.versionCli; return this.settings.versionCli;
}, },
nodeJsVersion(): string {
return this.settings.nodeJsVersion;
},
concurrency(): number {
return this.settings.concurrency;
},
isPublicApiEnabled(): boolean { isPublicApiEnabled(): boolean {
return this.api.enabled; return this.api.enabled;
}, },
@ -269,6 +304,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
this.setAllowedModules(settings.allowedModules); this.setAllowedModules(settings.allowedModules);
this.setSaveDataErrorExecution(settings.saveDataErrorExecution); this.setSaveDataErrorExecution(settings.saveDataErrorExecution);
this.setSaveDataSuccessExecution(settings.saveDataSuccessExecution); this.setSaveDataSuccessExecution(settings.saveDataSuccessExecution);
this.setSaveDataProgressExecution(settings.saveExecutionProgress);
this.setSaveManualExecutions(settings.saveManualExecutions); this.setSaveManualExecutions(settings.saveManualExecutions);
rootStore.setUrlBaseWebhook(settings.urlBaseWebhook); rootStore.setUrlBaseWebhook(settings.urlBaseWebhook);
@ -357,15 +393,18 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
const rootStore = useRootStore(); const rootStore = useRootStore();
return await runLdapSync(rootStore.restApiContext, data); return await runLdapSync(rootStore.restApiContext, data);
}, },
setSaveDataErrorExecution(newValue: string) { setSaveDataErrorExecution(newValue: WorkflowSettings.SaveDataExecution) {
this.saveDataErrorExecution = newValue; this.saveDataErrorExecution = newValue;
}, },
setSaveDataSuccessExecution(newValue: string) { setSaveDataSuccessExecution(newValue: WorkflowSettings.SaveDataExecution) {
this.saveDataSuccessExecution = newValue; this.saveDataSuccessExecution = newValue;
}, },
setSaveManualExecutions(saveManualExecutions: boolean) { setSaveManualExecutions(saveManualExecutions: boolean) {
this.saveManualExecutions = saveManualExecutions; this.saveManualExecutions = saveManualExecutions;
}, },
setSaveDataProgressExecution(newValue: boolean) {
this.saveDataProgressExecution = newValue;
},
async getTimezones(): Promise<IDataObject> { async getTimezones(): Promise<IDataObject> {
const rootStore = useRootStore(); const rootStore = useRootStore();
return await makeRestApiRequest(rootStore.restApiContext, 'GET', '/options/timezones'); return await makeRestApiRequest(rootStore.restApiContext, 'GET', '/options/timezones');

View file

@ -2560,6 +2560,8 @@ export type ExpressionEvaluatorType = 'tmpl' | 'tournament';
export type N8nAIProviderType = 'openai' | 'unknown'; export type N8nAIProviderType = 'openai' | 'unknown';
export interface IN8nUISettings { export interface IN8nUISettings {
isDocker: boolean;
databaseType: 'sqlite' | 'mariadb' | 'mysqldb' | 'postgresdb';
endpointForm: string; endpointForm: string;
endpointFormTest: string; endpointFormTest: string;
endpointFormWaiting: string; endpointFormWaiting: string;
@ -2568,6 +2570,7 @@ export interface IN8nUISettings {
saveDataErrorExecution: WorkflowSettings.SaveDataExecution; saveDataErrorExecution: WorkflowSettings.SaveDataExecution;
saveDataSuccessExecution: WorkflowSettings.SaveDataExecution; saveDataSuccessExecution: WorkflowSettings.SaveDataExecution;
saveManualExecutions: boolean; saveManualExecutions: boolean;
saveExecutionProgress: boolean;
executionTimeout: number; executionTimeout: number;
maxExecutionTimeout: number; maxExecutionTimeout: number;
workflowCallerPolicyDefaultOption: WorkflowSettings.CallerPolicy; workflowCallerPolicyDefaultOption: WorkflowSettings.CallerPolicy;
@ -2579,10 +2582,12 @@ export interface IN8nUISettings {
urlBaseWebhook: string; urlBaseWebhook: string;
urlBaseEditor: string; urlBaseEditor: string;
versionCli: string; versionCli: string;
nodeJsVersion: string;
concurrency: number;
authCookie: { authCookie: {
secure: boolean; secure: boolean;
}; };
binaryDataMode: string; binaryDataMode: 'default' | 'filesystem' | 's3';
releaseChannel: 'stable' | 'beta' | 'nightly' | 'dev'; releaseChannel: 'stable' | 'beta' | 'nightly' | 'dev';
n8nMetadata?: { n8nMetadata?: {
userId?: string; userId?: string;
@ -2658,6 +2663,8 @@ export interface IN8nUISettings {
}; };
hideUsagePage: boolean; hideUsagePage: boolean;
license: { license: {
planName?: string;
consumerId: string;
environment: 'development' | 'production' | 'staging'; environment: 'development' | 'production' | 'staging';
}; };
variables: { variables: {
@ -2683,6 +2690,14 @@ export interface IN8nUISettings {
pruneTime: number; pruneTime: number;
licensePruneTime: number; licensePruneTime: number;
}; };
pruning: {
isEnabled: boolean;
maxAge: number;
maxCount: number;
};
security: {
blockFileAccessToN8nFiles: boolean;
};
} }
export interface SecretsHelpersBase { export interface SecretsHelpersBase {