mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
feat: Introduce debug info button (#9895)
This commit is contained in:
parent
ae67d6b753
commit
be9a247577
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -68,6 +68,7 @@ export const SETTINGS_STORE_DEFAULT_STATE: ISettingsState = {
|
|||
onboardingCallPromptEnabled: false,
|
||||
saveDataErrorExecution: 'all',
|
||||
saveDataSuccessExecution: 'all',
|
||||
saveDataProgressExecution: false,
|
||||
saveManualExecutions: false,
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
148
packages/editor-ui/src/composables/useDebugInfo.ts
Normal file
148
packages/editor-ui/src/composables/useDebugInfo.ts
Normal 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,
|
||||
};
|
||||
}
|
|
@ -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": "We’re 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",
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue