feat: Add report bug buttons (#11304)

Co-authored-by: Cornelius Suermann <cornelius@n8n.io>
This commit is contained in:
Mutasem Aldmour 2024-10-21 13:32:37 +02:00 committed by GitHub
parent ba2827e7bb
commit 296f68f041
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 373 additions and 20 deletions

View file

@ -28,6 +28,7 @@ export function useDeviceSupport() {
} }
return { return {
userAgent: userAgent.value,
isTouchDevice: isTouchDevice.value, isTouchDevice: isTouchDevice.value,
isMacOs: isMacOs.value, isMacOs: isMacOs.value,
controlKeyCode: controlKeyCode.value, controlKeyCode: controlKeyCode.value,

View file

@ -14,7 +14,7 @@
"format:check": "biome ci . && prettier --check . --ignore-path ../../.prettierignore", "format:check": "biome ci . && prettier --check . --ignore-path ../../.prettierignore",
"serve": "cross-env VUE_APP_URL_BASE_API=http://localhost:5678/ vite --host 0.0.0.0 --port 8080 dev", "serve": "cross-env VUE_APP_URL_BASE_API=http://localhost:5678/ vite --host 0.0.0.0 --port 8080 dev",
"test": "vitest run", "test": "vitest run",
"test:dev": "vitest" "test:dev": "vitest --silent=false"
}, },
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.16.0", "@codemirror/autocomplete": "^6.16.0",

View file

@ -4,10 +4,10 @@ import type { Placement } from 'element-plus';
interface Props { interface Props {
label: string; label: string;
shortcut: KeyboardShortcut; shortcut?: KeyboardShortcut;
placement?: Placement; placement?: Placement;
} }
withDefaults(defineProps<Props>(), { placement: 'top' }); withDefaults(defineProps<Props>(), { placement: 'top', shortcut: undefined });
</script> </script>
<template> <template>
@ -15,7 +15,7 @@ withDefaults(defineProps<Props>(), { placement: 'top' });
<template #content> <template #content>
<div :class="$style.shortcut"> <div :class="$style.shortcut">
<div :class="$style.label">{{ label }}</div> <div :class="$style.label">{{ label }}</div>
<n8n-keyboard-shortcut v-bind="shortcut"></n8n-keyboard-shortcut> <n8n-keyboard-shortcut v-if="shortcut" v-bind="shortcut" />
</div> </div>
</template> </template>
<slot /> <slot />

View file

@ -20,6 +20,7 @@ import { useTelemetry } from '@/composables/useTelemetry';
import { useUserHelpers } from '@/composables/useUserHelpers'; import { useUserHelpers } from '@/composables/useUserHelpers';
import { ABOUT_MODAL_KEY, VERSIONS_MODAL_KEY, VIEWS } from '@/constants'; import { ABOUT_MODAL_KEY, VERSIONS_MODAL_KEY, VIEWS } from '@/constants';
import { useBugReporting } from '@/composables/useBugReporting';
const becomeTemplateCreatorStore = useBecomeTemplateCreatorStore(); const becomeTemplateCreatorStore = useBecomeTemplateCreatorStore();
const cloudPlanStore = useCloudPlanStore(); const cloudPlanStore = useCloudPlanStore();
@ -37,6 +38,7 @@ const locale = useI18n();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const telemetry = useTelemetry(); const telemetry = useTelemetry();
const { getReportingURL } = useBugReporting();
useUserHelpers(router, route); useUserHelpers(router, route);
@ -143,6 +145,15 @@ const mainMenuItems = ref([
target: '_blank', target: '_blank',
}, },
}, },
{
id: 'report-bug',
icon: 'bug',
label: locale.baseText('mainSidebar.helpMenuItems.reportBug'),
link: {
href: getReportingURL(),
target: '_blank',
},
},
{ {
id: 'about', id: 'about',
icon: 'info', icon: 'info',

View file

@ -82,6 +82,7 @@ const props = withDefaults(
readOnly?: boolean; readOnly?: boolean;
executing?: boolean; executing?: boolean;
keyBindings?: boolean; keyBindings?: boolean;
showBugReportingButton?: boolean;
}>(), }>(),
{ {
id: 'canvas', id: 'canvas',
@ -592,6 +593,7 @@ provide(CanvasKey, {
:class="$style.canvasControls" :class="$style.canvasControls"
:position="controlsPosition" :position="controlsPosition"
:show-interactive="false" :show-interactive="false"
:show-bug-reporting-button="showBugReportingButton"
:zoom="zoom" :zoom="zoom"
@zoom-to-fit="onFitView" @zoom-to-fit="onFitView"
@zoom-in="onZoomIn" @zoom-in="onZoomIn"

View file

@ -22,6 +22,7 @@ const props = withDefaults(
eventBus?: EventBus<CanvasEventBusEvents>; eventBus?: EventBus<CanvasEventBusEvents>;
readOnly?: boolean; readOnly?: boolean;
executing?: boolean; executing?: boolean;
showBugReportingButton?: boolean;
}>(), }>(),
{ {
id: 'canvas', id: 'canvas',
@ -57,6 +58,7 @@ const { nodes: mappedNodes, connections: mappedConnections } = useCanvasMapping(
v-if="workflow" v-if="workflow"
:nodes="mappedNodes" :nodes="mappedNodes"
:connections="mappedConnections" :connections="mappedConnections"
:show-bug-reporting-button="showBugReportingButton"
:event-bus="eventBus" :event-bus="eventBus"
:read-only="readOnly" :read-only="readOnly"
v-bind="$attrs" v-bind="$attrs"

View file

@ -1,15 +1,43 @@
import { createComponentRenderer } from '@/__tests__/render'; import { createComponentRenderer } from '@/__tests__/render';
import CanvasControlButtons from './CanvasControlButtons.vue'; import CanvasControlButtons from './CanvasControlButtons.vue';
import { setActivePinia } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
const MOCK_URL = 'mock-url';
vi.mock('@/composables/useBugReporting', () => ({
useBugReporting: () => ({ getReportingURL: () => MOCK_URL }),
}));
const renderComponent = createComponentRenderer(CanvasControlButtons); const renderComponent = createComponentRenderer(CanvasControlButtons);
describe('CanvasControlButtons', () => { describe('CanvasControlButtons', () => {
beforeAll(() => {
setActivePinia(createTestingPinia());
});
it('should render correctly', () => { it('should render correctly', () => {
const wrapper = renderComponent({
props: {
showBugReportingButton: true,
},
});
expect(wrapper.getByTestId('zoom-in-button')).toBeVisible();
expect(wrapper.getByTestId('zoom-out-button')).toBeVisible();
expect(wrapper.getByTestId('zoom-to-fit')).toBeVisible();
expect(wrapper.getByTestId('report-bug')).toBeVisible();
expect(wrapper.html()).toMatchSnapshot();
});
it('should render correctly without bug reporting button', () => {
const wrapper = renderComponent(); const wrapper = renderComponent();
expect(wrapper.getByTestId('zoom-in-button')).toBeVisible(); expect(wrapper.getByTestId('zoom-in-button')).toBeVisible();
expect(wrapper.getByTestId('zoom-out-button')).toBeVisible(); expect(wrapper.getByTestId('zoom-out-button')).toBeVisible();
expect(wrapper.getByTestId('zoom-to-fit')).toBeVisible(); expect(wrapper.getByTestId('zoom-to-fit')).toBeVisible();
expect(wrapper.queryByTestId('report-bug')).not.toBeInTheDocument();
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });

View file

@ -2,13 +2,17 @@
import { Controls } from '@vue-flow/controls'; import { Controls } from '@vue-flow/controls';
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue'; import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
import { computed } from 'vue'; import { computed } from 'vue';
import { useBugReporting } from '@/composables/useBugReporting';
import { useTelemetry } from '@/composables/useTelemetry';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
zoom?: number; zoom?: number;
showBugReportingButton?: boolean;
}>(), }>(),
{ {
zoom: 1, zoom: 1,
showBugReportingButton: false,
}, },
); );
@ -19,6 +23,9 @@ const emit = defineEmits<{
'zoom-to-fit': []; 'zoom-to-fit': [];
}>(); }>();
const { getReportingURL } = useBugReporting();
const telemetry = useTelemetry();
const isResetZoomVisible = computed(() => props.zoom !== 1); const isResetZoomVisible = computed(() => props.zoom !== 1);
function onResetZoom() { function onResetZoom() {
@ -36,6 +43,10 @@ function onZoomOut() {
function onZoomToFit() { function onZoomToFit() {
emit('zoom-to-fit'); emit('zoom-to-fit');
} }
function trackBugReport() {
telemetry.track('User clicked bug report button in canvas', {}, { withPostHog: true });
}
</script> </script>
<template> <template>
<Controls :show-zoom="false" :show-fit-view="false"> <Controls :show-zoom="false" :show-fit-view="false">
@ -88,6 +99,14 @@ function onZoomToFit() {
@click="onResetZoom" @click="onResetZoom"
/> />
</KeyboardShortcutTooltip> </KeyboardShortcutTooltip>
<KeyboardShortcutTooltip
v-if="props.showBugReportingButton"
:label="$locale.baseText('nodeView.reportBug')"
>
<a :href="getReportingURL()" target="_blank" @click="trackBugReport">
<N8nIconButton type="tertiary" size="large" icon="bug" data-test-id="report-bug" />
</a>
</KeyboardShortcutTooltip>
</Controls> </Controls>
</template> </template>

View file

@ -20,6 +20,35 @@ exports[`CanvasControlButtons > should render correctly 1`] = `
</button> </button>
<!--teleport start--> <!--teleport start-->
<!--teleport end--> <!--teleport end-->
<!--v-if--><a href="mock-url" target="_blank" class="el-tooltip__trigger"><button class="button button tertiary large withIcon square" aria-live="polite" data-test-id="report-bug"><span class="icon"><span class="n8n-text compact size-large regular n8n-icon n8n-icon"><svg class="svg-inline--fa fa-bug fa-w-16 large" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="bug" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path class="" fill="currentColor" d="M511.988 288.9c-.478 17.43-15.217 31.1-32.653 31.1H424v16c0 21.864-4.882 42.584-13.6 61.145l60.228 60.228c12.496 12.497 12.496 32.758 0 45.255-12.498 12.497-32.759 12.496-45.256 0l-54.736-54.736C345.886 467.965 314.351 480 280 480V236c0-6.627-5.373-12-12-12h-24c-6.627 0-12 5.373-12 12v244c-34.351 0-65.886-12.035-90.636-32.108l-54.736 54.736c-12.498 12.497-32.759 12.496-45.256 0-12.496-12.497-12.496-32.758 0-45.255l60.228-60.228C92.882 378.584 88 357.864 88 336v-16H32.666C15.23 320 .491 306.33.013 288.9-.484 270.816 14.028 256 32 256h56v-58.745l-46.628-46.628c-12.496-12.497-12.496-32.758 0-45.255 12.498-12.497 32.758-12.497 45.256 0L141.255 160h229.489l54.627-54.627c12.498-12.497 32.758-12.497 45.256 0 12.496 12.497 12.496 32.758 0 45.255L424 197.255V256h56c17.972 0 32.484 14.816 31.988 32.9zM257 0c-61.856 0-112 50.144-112 112h224C369 50.144 318.856 0 257 0z"></path></svg></span></span>
<!--v-if-->
</button></a>
<!--teleport start-->
<!--teleport end-->
</div>"
`;
exports[`CanvasControlButtons > should render correctly without bug reporting button 1`] = `
"<div class="vue-flow__panel bottom left vue-flow__controls" style="pointer-events: all;">
<!---->
<!----><button class="vue-flow__controls-button vue-flow__controls-interactive"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 32">
<path d="M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0c-4.114 1.828-1.37 2.133.305 2.438 1.676.305 4.42 2.59 4.42 5.181v3.048H3.047A3.056 3.056 0 0 0 0 13.714v15.238A3.056 3.056 0 0 0 3.048 32h18.285a3.056 3.056 0 0 0 3.048-3.048V13.714a3.056 3.056 0 0 0-3.048-3.047zM12.19 24.533a3.056 3.056 0 0 1-3.047-3.047 3.056 3.056 0 0 1 3.047-3.048 3.056 3.056 0 0 1 3.048 3.048 3.056 3.056 0 0 1-3.048 3.047z"></path>
</svg>
<!---->
</button><button class="button button tertiary large withIcon square el-tooltip__trigger el-tooltip__trigger" aria-live="polite" data-test-id="zoom-to-fit"><span class="icon"><span class="n8n-text compact size-large regular n8n-icon n8n-icon"><svg class="svg-inline--fa fa-expand fa-w-14 large" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="expand" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path class="" fill="currentColor" d="M0 180V56c0-13.3 10.7-24 24-24h124c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12H64v84c0 6.6-5.4 12-12 12H12c-6.6 0-12-5.4-12-12zM288 44v40c0 6.6 5.4 12 12 12h84v84c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12V56c0-13.3-10.7-24-24-24H300c-6.6 0-12 5.4-12 12zm148 276h-40c-6.6 0-12 5.4-12 12v84h-84c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h124c13.3 0 24-10.7 24-24V332c0-6.6-5.4-12-12-12zM160 468v-40c0-6.6-5.4-12-12-12H64v-84c0-6.6-5.4-12-12-12H12c-6.6 0-12 5.4-12 12v124c0 13.3 10.7 24 24 24h124c6.6 0 12-5.4 12-12z"></path></svg></span></span>
<!--v-if-->
</button>
<!--teleport start-->
<!--teleport end--><button class="button button tertiary large withIcon square el-tooltip__trigger el-tooltip__trigger" aria-live="polite" data-test-id="zoom-in-button"><span class="icon"><span class="n8n-text compact size-large regular n8n-icon n8n-icon"><svg class="svg-inline--fa fa-search-plus fa-w-16 large" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="search-plus" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path class="" fill="currentColor" d="M304 192v32c0 6.6-5.4 12-12 12h-56v56c0 6.6-5.4 12-12 12h-32c-6.6 0-12-5.4-12-12v-56h-56c-6.6 0-12-5.4-12-12v-32c0-6.6 5.4-12 12-12h56v-56c0-6.6 5.4-12 12-12h32c6.6 0 12 5.4 12 12v56h56c6.6 0 12 5.4 12 12zm201 284.7L476.7 505c-9.4 9.4-24.6 9.4-33.9 0L343 405.3c-4.5-4.5-7-10.6-7-17V372c-35.3 27.6-79.7 44-128 44C93.1 416 0 322.9 0 208S93.1 0 208 0s208 93.1 208 208c0 48.3-16.4 92.7-44 128h16.3c6.4 0 12.5 2.5 17 7l99.7 99.7c9.3 9.4 9.3 24.6 0 34zM344 208c0-75.2-60.8-136-136-136S72 132.8 72 208s60.8 136 136 136 136-60.8 136-136z"></path></svg></span></span>
<!--v-if-->
</button>
<!--teleport start-->
<!--teleport end--><button class="button button tertiary large withIcon square el-tooltip__trigger el-tooltip__trigger" aria-live="polite" data-test-id="zoom-out-button"><span class="icon"><span class="n8n-text compact size-large regular n8n-icon n8n-icon"><svg class="svg-inline--fa fa-search-minus fa-w-16 large" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="search-minus" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path class="" fill="currentColor" d="M304 192v32c0 6.6-5.4 12-12 12H124c-6.6 0-12-5.4-12-12v-32c0-6.6 5.4-12 12-12h168c6.6 0 12 5.4 12 12zm201 284.7L476.7 505c-9.4 9.4-24.6 9.4-33.9 0L343 405.3c-4.5-4.5-7-10.6-7-17V372c-35.3 27.6-79.7 44-128 44C93.1 416 0 322.9 0 208S93.1 0 208 0s208 93.1 208 208c0 48.3-16.4 92.7-44 128h16.3c6.4 0 12.5 2.5 17 7l99.7 99.7c9.3 9.4 9.3 24.6 0 34zM344 208c0-75.2-60.8-136-136-136S72 132.8 72 208s60.8 136 136 136 136-60.8 136-136z"></path></svg></span></span>
<!--v-if-->
</button>
<!--teleport start-->
<!--teleport end-->
<!--v-if-->
<!--v-if--> <!--v-if-->
</div>" </div>"
`; `;

View file

@ -0,0 +1,3 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`useBugReporting > should generate the correct reporting URL 1`] = `"https://github.com/n8n-io/n8n/issues/new?labels=bug-report&body=%0A%3C%21--+Please+follow+the+template+below.+Skip+the+questions+that+are+not+relevant+to+you.+--%3E%0A%0A%23%23+Describe+the+problem%2Ferror%2Fquestion%0A%0A%0A%23%23+What+is+the+error+message+%28if+any%29%3F%0A%0A%0A%23%23+Please+share+your+workflow%2Fscreenshots%2Frecording%0A%0A%60%60%60%0A%28Select+the+nodes+on+your+canvas+and+use+the+keyboard+shortcuts+CMD%2BC%2FCTRL%2BC+and+CMD%2BV%2FCTRL%2BV+to+copy+and+paste+the+workflow.%29%0A%60%60%60%0A%0A%0A%23%23+Share+the+output+returned+by+the+last+node%0A%3C%21--+If+you+need+help+with+data+transformations%2C+please+also+share+your+expected+output.+--%3E%0A%0A%0Amocked+debug+info%7D"`;

View file

@ -0,0 +1,42 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`useDebugInfo > should generate debug info 1`] = `
"# Debug info
## core
- n8nVersion: 0.123.0
- platform: docker (cloud)
- nodeJsVersion: 14.17.0
- database: postgres
- executionMode: regular
- concurrency: 10
- license: community
- consumerId: consumer-123
## storage
- success: all
- error: none
- progress: true
- manual: true
- binaryMode: memory
## pruning
- enabled: true
- maxAge: 24 hours
- maxCount: 100 executions
## client
- userAgent: Mozilla/5.0
- isTouchDevice: false
## security
- blockFileAccessToN8nFiles: false
- secureCookie: false
Generated at: 2024-06-05T15:40:04.819Z"
`;

View file

@ -0,0 +1,18 @@
import { describe, it, expect, vi } from 'vitest';
import { useBugReporting } from './useBugReporting';
vi.mock('@/composables/useDebugInfo', () => ({
useDebugInfo: () => ({
generateDebugInfo: () => 'mocked debug info',
}),
}));
describe('useBugReporting', () => {
it('should generate the correct reporting URL', () => {
const { getReportingURL } = useBugReporting();
const url = getReportingURL();
expect(url).toContain('mocked+debug+info');
expect(url).toMatchSnapshot();
});
});

View file

@ -0,0 +1,41 @@
import { useDebugInfo } from '@/composables/useDebugInfo';
const BASE_FORUM_URL = 'https://github.com/n8n-io/n8n/issues/new?labels=bug-report';
const REPORT_TEMPLATE = `
<!-- Please follow the template below. Skip the questions that are not relevant to you. -->
## Describe the problem/error/question
## What is the error message (if any)?
## Please share your workflow/screenshots/recording
\`\`\`
(Select the nodes on your canvas and use the keyboard shortcuts CMD+C/CTRL+C and CMD+V/CTRL+V to copy and paste the workflow.)
\`\`\`
## Share the output returned by the last node
<!-- If you need help with data transformations, please also share your expected output. -->
`;
export function useBugReporting() {
const debugInfo = useDebugInfo();
const getReportingURL = () => {
const url = new URL(BASE_FORUM_URL);
const report = `${REPORT_TEMPLATE}\n${debugInfo.generateDebugInfo({ skipSensitive: true, secondaryHeader: true })}}`;
url.searchParams.append('body', report);
return url.toString();
};
return {
getReportingURL,
};
}

View file

@ -0,0 +1,117 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { useDebugInfo } from './useDebugInfo';
import type { RootState } from '@/Interface';
import type { useSettingsStore as useSettingsStoreType } from '@/stores/settings.store';
import type { RecursivePartial } from '@/type-utils';
vi.mock('@/stores/root.store', () => ({
useRootStore: (): Partial<RootState> => ({
versionCli: '0.123.0',
}),
}));
const MOCK_BASE_SETTINGS: RecursivePartial<ReturnType<typeof useSettingsStoreType>> = {
isDocker: true,
deploymentType: 'cloud',
nodeJsVersion: '14.17.0',
databaseType: 'postgresdb',
isQueueModeEnabled: false,
settings: {
concurrency: 10,
license: {
consumerId: 'consumer-id',
environment: 'production',
},
},
isCommunityPlan: true,
consumerId: 'consumer-123',
saveDataSuccessExecution: 'all',
saveDataErrorExecution: 'none',
saveDataProgressExecution: true,
saveManualExecutions: true,
binaryDataMode: 'default',
pruning: {
isEnabled: true,
maxAge: 24,
maxCount: 100,
},
security: {
blockFileAccessToN8nFiles: false,
secureCookie: false,
},
};
const { useSettingsStore } = vi.hoisted(() => ({
useSettingsStore: vi.fn(),
}));
vi.mock('@/stores/settings.store', () => ({
useSettingsStore,
}));
vi.mock('n8n-design-system', () => ({
useDeviceSupport: () => ({
isTouchDevice: false,
userAgent: 'Mozilla/5.0',
}),
}));
const NOW = 1717602004819;
vi.useFakeTimers({
now: NOW,
});
describe('useDebugInfo', () => {
beforeEach(() => {
useSettingsStore.mockReturnValue(MOCK_BASE_SETTINGS);
});
it('should generate debug info', () => {
const { generateDebugInfo } = useDebugInfo();
const debugInfo = generateDebugInfo();
expect(debugInfo).toMatchSnapshot();
});
it('should generate debug info without sensitive data', () => {
const { generateDebugInfo } = useDebugInfo();
const debugInfo = generateDebugInfo({ skipSensitive: true });
expect(debugInfo).not.toContain('consumerId');
expect(debugInfo).toContain('Generated at:');
});
it('should include security info if insecure settings are found', () => {
const { generateDebugInfo } = useDebugInfo();
const debugInfo = generateDebugInfo();
expect(debugInfo).toContain('blockFileAccessToN8nFiles: false');
expect(debugInfo).toContain('secureCookie: false');
});
it('should not include security info if all settings are secure', () => {
useSettingsStore.mockReturnValue({
...MOCK_BASE_SETTINGS,
security: {
...MOCK_BASE_SETTINGS.security,
blockFileAccessToN8nFiles: true,
secureCookie: true,
},
});
const { generateDebugInfo } = useDebugInfo();
const debugInfo = generateDebugInfo();
expect(debugInfo).not.toContain('blockFileAccessToN8nFiles');
expect(debugInfo).not.toContain('secureCookie');
});
it('should generate markdown with secondary headers', () => {
const { generateDebugInfo } = useDebugInfo();
const debugInfo = generateDebugInfo({ secondaryHeader: true });
expect(debugInfo).toContain('### core');
expect(debugInfo).toContain('## Debug info');
});
});

View file

@ -1,5 +1,6 @@
import { useRootStore } from '@/stores/root.store'; import { useRootStore } from '@/stores/root.store';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useDeviceSupport } from 'n8n-design-system';
import type { WorkflowSettings } from 'n8n-workflow'; import type { WorkflowSettings } from 'n8n-workflow';
type DebugInfo = { type DebugInfo = {
@ -10,7 +11,7 @@ type DebugInfo = {
database: 'sqlite' | 'mysql' | 'mariadb' | 'postgres'; database: 'sqlite' | 'mysql' | 'mariadb' | 'postgres';
executionMode: 'regular' | 'scaling'; executionMode: 'regular' | 'scaling';
license: 'community' | 'enterprise (production)' | 'enterprise (sandbox)'; license: 'community' | 'enterprise (production)' | 'enterprise (sandbox)';
consumerId: string; consumerId?: string;
concurrency: number; concurrency: number;
}; };
storage: { storage: {
@ -36,14 +37,19 @@ type DebugInfo = {
secureCookie?: boolean; secureCookie?: boolean;
blockFileAccessToN8nFiles?: boolean; blockFileAccessToN8nFiles?: boolean;
}; };
client: {
userAgent: string;
isTouchDevice: boolean;
};
}; };
export function useDebugInfo() { export function useDebugInfo() {
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const rootStore = useRootStore(); const rootStore = useRootStore();
const { isTouchDevice, userAgent } = useDeviceSupport();
const coreInfo = () => { const coreInfo = (skipSensitive?: boolean) => {
return { const info = {
n8nVersion: rootStore.versionCli, n8nVersion: rootStore.versionCli,
platform: platform:
settingsStore.isDocker && settingsStore.deploymentType === 'cloud' settingsStore.isDocker && settingsStore.deploymentType === 'cloud'
@ -60,13 +66,22 @@ export function useDebugInfo() {
: settingsStore.databaseType, : settingsStore.databaseType,
executionMode: settingsStore.isQueueModeEnabled ? 'scaling' : 'regular', executionMode: settingsStore.isQueueModeEnabled ? 'scaling' : 'regular',
concurrency: settingsStore.settings.concurrency, concurrency: settingsStore.settings.concurrency,
license: settingsStore.isCommunityPlan license:
settingsStore.isCommunityPlan || !settingsStore.settings.license
? 'community' ? 'community'
: settingsStore.settings.license.environment === 'production' : settingsStore.settings.license.environment === 'production'
? 'enterprise (production)' ? 'enterprise (production)'
: 'enterprise (sandbox)', : 'enterprise (sandbox)',
consumerId: settingsStore.consumerId,
} as const; } as const;
if (!skipSensitive) {
return {
...info,
consumerId: !skipSensitive ? settingsStore.consumerId : undefined,
};
}
return info;
}; };
const storageInfo = (): DebugInfo['storage'] => { const storageInfo = (): DebugInfo['storage'] => {
@ -101,11 +116,19 @@ export function useDebugInfo() {
return info; return info;
}; };
const gatherDebugInfo = () => { const client = (): DebugInfo['client'] => {
return {
userAgent,
isTouchDevice,
};
};
const gatherDebugInfo = (skipSensitive?: boolean) => {
const debugInfo: DebugInfo = { const debugInfo: DebugInfo = {
core: coreInfo(), core: coreInfo(skipSensitive),
storage: storageInfo(), storage: storageInfo(),
pruning: pruningInfo(), pruning: pruningInfo(),
client: client(),
}; };
const security = securityInfo(); const security = securityInfo();
@ -115,11 +138,15 @@ export function useDebugInfo() {
return debugInfo; return debugInfo;
}; };
const toMarkdown = (debugInfo: DebugInfo): string => { const toMarkdown = (
let markdown = '# Debug info\n\n'; debugInfo: DebugInfo,
{ secondaryHeader }: { secondaryHeader?: boolean },
): string => {
const extraLevel = secondaryHeader ? '#' : '';
let markdown = `${extraLevel}# Debug info\n\n`;
for (const sectionKey in debugInfo) { for (const sectionKey in debugInfo) {
markdown += `## ${sectionKey}\n\n`; markdown += `${extraLevel}## ${sectionKey}\n\n`;
const section = debugInfo[sectionKey as keyof DebugInfo]; const section = debugInfo[sectionKey as keyof DebugInfo];
@ -140,8 +167,11 @@ export function useDebugInfo() {
return `${markdown}Generated at: ${new Date().toISOString()}`; return `${markdown}Generated at: ${new Date().toISOString()}`;
}; };
const generateDebugInfo = () => { const generateDebugInfo = ({
return appendTimestamp(toMarkdown(gatherDebugInfo())); skipSensitive,
secondaryHeader,
}: { skipSensitive?: boolean; secondaryHeader?: boolean } = {}) => {
return appendTimestamp(toMarkdown(gatherDebugInfo(skipSensitive), { secondaryHeader }));
}; };
return { return {

View file

@ -876,6 +876,7 @@
"mainSidebar.helpMenuItems.documentation": "Documentation", "mainSidebar.helpMenuItems.documentation": "Documentation",
"mainSidebar.helpMenuItems.forum": "Forum", "mainSidebar.helpMenuItems.forum": "Forum",
"mainSidebar.helpMenuItems.quickstart": "Quickstart", "mainSidebar.helpMenuItems.quickstart": "Quickstart",
"mainSidebar.helpMenuItems.reportBug": "Report a bug",
"mainSidebar.new": "New", "mainSidebar.new": "New",
"mainSidebar.newTemplate": "New from template", "mainSidebar.newTemplate": "New from template",
"mainSidebar.open": "Open", "mainSidebar.open": "Open",
@ -1274,6 +1275,7 @@
"nodeView.redirecting": "Redirecting", "nodeView.redirecting": "Redirecting",
"nodeView.refresh": "Refresh", "nodeView.refresh": "Refresh",
"nodeView.resetZoom": "Reset Zoom", "nodeView.resetZoom": "Reset Zoom",
"nodeView.reportBug": "Report a bug",
"nodeView.runButtonText.executeWorkflow": "Test workflow", "nodeView.runButtonText.executeWorkflow": "Test workflow",
"nodeView.runButtonText.executingWorkflow": "Executing workflow", "nodeView.runButtonText.executingWorkflow": "Executing workflow",
"nodeView.runButtonText.waitingForTriggerEvent": "Waiting for trigger event", "nodeView.runButtonText.waitingForTriggerEvent": "Waiting for trigger event",

View file

@ -0,0 +1,7 @@
export type RecursivePartial<T> = {
[P in keyof T]?: T[P] extends Array<infer U>
? Array<RecursivePartial<U>>
: T[P] extends object | undefined
? RecursivePartial<T[P]>
: T[P];
};

View file

@ -1572,6 +1572,7 @@ onBeforeUnmount(() => {
:event-bus="canvasEventBus" :event-bus="canvasEventBus"
:read-only="isCanvasReadOnly" :read-only="isCanvasReadOnly"
:executing="isWorkflowRunning" :executing="isWorkflowRunning"
:show-bug-reporting-button="!isDemoRoute || !!executionsStore.activeExecution"
:key-bindings="keyBindingsEnabled" :key-bindings="keyBindingsEnabled"
@update:nodes:position="onUpdateNodesPosition" @update:nodes:position="onUpdateNodesPosition"
@update:node:position="onUpdateNodePosition" @update:node:position="onUpdateNodePosition"