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
getUsersLimit() {
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 uniq from 'lodash/uniq';
import { createWriteStream } from 'fs';
@ -83,6 +84,8 @@ export class FrontendService {
}
this.settings = {
isDocker: this.isDocker(),
databaseType: config.getEnv('database.type'),
previewMode: process.env.N8N_PREVIEW_MODE === 'true',
endpointForm: config.getEnv('endpoints.form'),
endpointFormTest: config.getEnv('endpoints.formTest'),
@ -92,6 +95,7 @@ export class FrontendService {
saveDataErrorExecution: config.getEnv('executions.saveDataOnError'),
saveDataSuccessExecution: config.getEnv('executions.saveDataOnSuccess'),
saveManualExecutions: config.getEnv('executions.saveDataManualExecutions'),
saveExecutionProgress: config.getEnv('executions.saveExecutionProgress'),
executionTimeout: config.getEnv('executions.timeout'),
maxExecutionTimeout: config.getEnv('executions.maxTimeout'),
workflowCallerPolicyDefaultOption: config.getEnv('workflows.callerPolicyDefaultOption'),
@ -99,7 +103,9 @@ export class FrontendService {
urlBaseWebhook: this.urlService.getWebhookBaseUrl(),
urlBaseEditor: instanceBaseUrl,
binaryDataMode: config.getEnv('binaryDataManager.mode'),
nodeJsVersion: process.version.replace(/^v/, ''),
versionCli: '',
concurrency: config.getEnv('executions.concurrency.productionLimit'),
authCookie: {
secure: config.getEnv('secure_cookie'),
},
@ -196,6 +202,7 @@ export class FrontendService {
},
hideUsagePage: config.getEnv('hideUsagePage'),
license: {
consumerId: 'unknown',
environment: config.getEnv('license.tenantId') === 1 ? 'production' : 'staging',
},
variables: {
@ -218,6 +225,14 @@ export class FrontendService {
pruneTime: -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 isS3Licensed = this.license.isBinaryDataS3Licensed();
this.settings.license.planName = this.license.getPlanName();
this.settings.license.consumerId = this.license.getConsumerId();
// refresh enterprise status
Object.assign(this.settings.enterprise, {
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;
instanceId: string;
isNpmAvailable: boolean;
binaryDataMode: string;
binaryDataMode: 'default' | 'filesystem' | 's3';
}
export interface NodeMetadataMap {
@ -1380,9 +1380,10 @@ export interface ISettingsState {
enabled: boolean;
};
onboardingCallPromptEnabled: boolean;
saveDataErrorExecution: string;
saveDataSuccessExecution: string;
saveDataErrorExecution: WorkflowSettings.SaveDataExecution;
saveDataSuccessExecution: WorkflowSettings.SaveDataExecution;
saveManualExecutions: boolean;
saveDataProgressExecution: boolean;
}
export type NodeTypesByTypeNameAndVersion = {

View file

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

View file

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

View file

@ -42,6 +42,16 @@
<n8n-text>{{ rootStore.instanceId }}</n8n-text>
</el-col>
</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>
</template>
@ -66,6 +76,9 @@ import Modal from './Modal.vue';
import { ABOUT_MODAL_KEY } from '../constants';
import { useSettingsStore } from '@/stores/settings.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({
name: 'About',
@ -85,6 +98,15 @@ export default defineComponent({
closeDialog() {
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>

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.sourceCode": "Source Code",
"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.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",

View file

@ -68,15 +68,50 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
saveDataErrorExecution: 'all',
saveDataSuccessExecution: 'all',
saveManualExecutions: false,
saveDataProgressExecution: false,
}),
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() {
return (feature: EnterpriseEditionFeatureValue): boolean =>
Boolean(this.settings.enterprise?.[feature]);
},
versionCli(): string {
return this.settings.versionCli;
},
nodeJsVersion(): string {
return this.settings.nodeJsVersion;
},
concurrency(): number {
return this.settings.concurrency;
},
isPublicApiEnabled(): boolean {
return this.api.enabled;
},
@ -269,6 +304,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
this.setAllowedModules(settings.allowedModules);
this.setSaveDataErrorExecution(settings.saveDataErrorExecution);
this.setSaveDataSuccessExecution(settings.saveDataSuccessExecution);
this.setSaveDataProgressExecution(settings.saveExecutionProgress);
this.setSaveManualExecutions(settings.saveManualExecutions);
rootStore.setUrlBaseWebhook(settings.urlBaseWebhook);
@ -357,15 +393,18 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
const rootStore = useRootStore();
return await runLdapSync(rootStore.restApiContext, data);
},
setSaveDataErrorExecution(newValue: string) {
setSaveDataErrorExecution(newValue: WorkflowSettings.SaveDataExecution) {
this.saveDataErrorExecution = newValue;
},
setSaveDataSuccessExecution(newValue: string) {
setSaveDataSuccessExecution(newValue: WorkflowSettings.SaveDataExecution) {
this.saveDataSuccessExecution = newValue;
},
setSaveManualExecutions(saveManualExecutions: boolean) {
this.saveManualExecutions = saveManualExecutions;
},
setSaveDataProgressExecution(newValue: boolean) {
this.saveDataProgressExecution = newValue;
},
async getTimezones(): Promise<IDataObject> {
const rootStore = useRootStore();
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 interface IN8nUISettings {
isDocker: boolean;
databaseType: 'sqlite' | 'mariadb' | 'mysqldb' | 'postgresdb';
endpointForm: string;
endpointFormTest: string;
endpointFormWaiting: string;
@ -2568,6 +2570,7 @@ export interface IN8nUISettings {
saveDataErrorExecution: WorkflowSettings.SaveDataExecution;
saveDataSuccessExecution: WorkflowSettings.SaveDataExecution;
saveManualExecutions: boolean;
saveExecutionProgress: boolean;
executionTimeout: number;
maxExecutionTimeout: number;
workflowCallerPolicyDefaultOption: WorkflowSettings.CallerPolicy;
@ -2579,10 +2582,12 @@ export interface IN8nUISettings {
urlBaseWebhook: string;
urlBaseEditor: string;
versionCli: string;
nodeJsVersion: string;
concurrency: number;
authCookie: {
secure: boolean;
};
binaryDataMode: string;
binaryDataMode: 'default' | 'filesystem' | 's3';
releaseChannel: 'stable' | 'beta' | 'nightly' | 'dev';
n8nMetadata?: {
userId?: string;
@ -2658,6 +2663,8 @@ export interface IN8nUISettings {
};
hideUsagePage: boolean;
license: {
planName?: string;
consumerId: string;
environment: 'development' | 'production' | 'staging';
};
variables: {
@ -2683,6 +2690,14 @@ export interface IN8nUISettings {
pruneTime: number;
licensePruneTime: number;
};
pruning: {
isEnabled: boolean;
maxAge: number;
maxCount: number;
};
security: {
blockFileAccessToN8nFiles: boolean;
};
}
export interface SecretsHelpersBase {