diff --git a/package-lock.json b/package-lock.json index 62fef86155..c59e3ca17f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22278,9 +22278,9 @@ } }, "node_modules/openai": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-3.3.0.tgz", - "integrity": "sha512-uqxI/Au+aPRnsaQRe8CojU0eCR7I0mBiKjD3sNMzY6DaC1ZVrc85u98mtJW6voDug8fgGN+DIZmTDxTthxb7dQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-3.2.1.tgz", + "integrity": "sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A==", "dependencies": { "axios": "^0.26.0", "form-data": "^4.0.0" diff --git a/src/components/organisms/NewProject/NewProject.tsx b/src/components/organisms/NewProject/NewProject.tsx index 214af4bf41..57f8e47e34 100644 --- a/src/components/organisms/NewProject/NewProject.tsx +++ b/src/components/organisms/NewProject/NewProject.tsx @@ -51,6 +51,7 @@ const NewProject: React.FC = () => { 'Not sure where to start? Explore a sample project containing everything you need to get started with just one click.', itemAction: () => { dispatch(openGitCloneModal({fromSampleProject: true})); + trackEvent('app_start/create_project', {from: 'sample'}); }, }, { @@ -105,6 +106,7 @@ const NewProject: React.FC = () => { itemDescription: 'Create a new project from a Helm Chart in a Helm repository, and save it locally.', itemAction: () => { dispatch(openHelmRepoModal()); + trackEvent('app_start/create_project', {from: 'helm'}); }, }, ]; diff --git a/src/editor/editor.instance.ts b/src/editor/editor.instance.ts index 17bbf791b8..f73ae2c834 100644 --- a/src/editor/editor.instance.ts +++ b/src/editor/editor.instance.ts @@ -19,6 +19,7 @@ let isRecreatingModel = false; let editorType: 'local' | 'cluster' | undefined; let hasTypedInEditor = false; let hasModelContentChanged = false; +let currentResourceKind: string | undefined; export const didEditorContentChange = () => hasModelContentChanged; @@ -36,7 +37,7 @@ export const mountEditor = (props: {element: HTMLElement; type: 'local' | 'clust if (hasTypedInEditor) { return; } - trackEvent('edit/code_changes', {from: getEditorType()}); + trackEvent('edit/code_changes', {from: getEditorType(), resourceKind: currentResourceKind}); hasTypedInEditor = true; }); EDITOR.onDidChangeModelContent(e => { @@ -81,6 +82,13 @@ export const setEditorNextSelection = (range: monaco.IRange) => { }; export function recreateEditorModel(editor: monaco.editor.ICodeEditor, text: string, language: string = 'yaml') { + const kindMatch = text.match(/kind:\s*(\w+)/); + if (kindMatch?.length === 1) { + currentResourceKind = kindMatch[1]; + } else { + currentResourceKind = undefined; + } + isRecreatingModel = true; resetEditor(); editor.getModel()?.dispose(); @@ -113,10 +121,14 @@ export const clearEditorLinks = () => { }; export const addEditorCommand = (payload: EditorCommand['payload'], supportHtml?: boolean) => { - const {text, altText, handler, beforeText, afterText} = payload; + const {text, altText, handler, beforeText, afterText, type} = payload; const id = `cmd_${uuidv4()}`; - const disposable: monaco.IDisposable = monaco.editor.registerCommand(id, handler); + const wrappedHandler = () => { + trackEvent('editor/run_command', {type, resourceKind: currentResourceKind}); + handler(); + }; + const disposable: monaco.IDisposable = monaco.editor.registerCommand(id, wrappedHandler); let markdownLink: monaco.IMarkdownString; @@ -170,11 +182,14 @@ monaco.languages.registerHoverProvider('yaml', { provideHover: (model, position) => { const positionHovers = editorHovers.filter(hover => isPositionInRange(position, hover.range)); if (positionHovers.length === 0) { + trackEvent('editor/hover', {resourceKind: currentResourceKind}); return null; } if (positionHovers.length === 1) { + trackEvent('editor/hover', {resourceKind: currentResourceKind, types: [positionHovers[0].type]}); return positionHovers[0]; } + trackEvent('editor/hover', {resourceKind: currentResourceKind, types: positionHovers.map(hover => hover.type)}); return { contents: positionHovers.map(hover => hover.contents).flat(), }; @@ -192,6 +207,12 @@ monaco.languages.registerLinkProvider('yaml', { }, resolveLink: async link => { const linksToResolve = editorLinks.filter(({range}) => isRangeInRange(range, link.range)); + if (linksToResolve.length > 0) { + trackEvent('editor/follow_link', { + resourceKind: currentResourceKind, + types: linksToResolve.map(l => l.type), + }); + } const promises = linksToResolve.map(({handler}) => Promise.resolve(handler())); await Promise.all(promises); return {range: link.range}; diff --git a/src/editor/editor.types.ts b/src/editor/editor.types.ts index a3b1ff8b89..7d14d942e1 100644 --- a/src/editor/editor.types.ts +++ b/src/editor/editor.types.ts @@ -1,11 +1,13 @@ import * as monaco from 'monaco-editor'; export type EditorHover = { + type: string; range: monaco.IRange; contents: monaco.IMarkdownString[]; }; export type EditorLink = { + type: string; range: monaco.IRange; tooltip?: string; handler: () => Promise | void; @@ -13,6 +15,7 @@ export type EditorLink = { export type EditorCommand = { payload: { + type: string; text: string; altText: string; handler: monaco.editor.ICommandHandler; diff --git a/src/editor/enhancers/helm/templates.ts b/src/editor/enhancers/helm/templates.ts index 1dbbc2514c..0c7dcf11b7 100644 --- a/src/editor/enhancers/helm/templates.ts +++ b/src/editor/enhancers/helm/templates.ts @@ -60,6 +60,7 @@ export const helmTemplateFileEnhancer = createEditorEnhancer(({state, resourceId typeof keyPathInFile.value === 'object' ? JSON.stringify(keyPathInFile.value, null, 4) : keyPathInFile.value; const newCommand = addEditorCommand({ + type: 'go_to_helm_values_file', text: `${keyPathInFile.filePath}`, altText: 'Select file', handler: () => { @@ -87,6 +88,7 @@ export const helmTemplateFileEnhancer = createEditorEnhancer(({state, resourceId const text = hasMultipleLinks ? `Found this value in ${keyPathsInFile.length} helm value files` : ``; if (!hasMultipleLinks) { addEditorLink({ + type: 'go_to_file', range: helmFileValue.range, tooltip: 'Open file', handler: () => { @@ -108,6 +110,7 @@ export const helmTemplateFileEnhancer = createEditorEnhancer(({state, resourceId if (commands.length) { addEditorHover({ + type: 'helm_template_values_found', range: helmFileValue.range, contents: [createMarkdownString(text), ...commands.map(c => c.markdownLink)], }); @@ -117,6 +120,7 @@ export const helmTemplateFileEnhancer = createEditorEnhancer(({state, resourceId } addEditorHover({ + type: 'helm_template_values_not_found', range: helmFileValue.range, contents: [createMarkdownString('This value was not found in any helm values file.')], }); diff --git a/src/editor/enhancers/helm/valuesFile.ts b/src/editor/enhancers/helm/valuesFile.ts index 023b8aa861..048416ab4f 100644 --- a/src/editor/enhancers/helm/valuesFile.ts +++ b/src/editor/enhancers/helm/valuesFile.ts @@ -40,6 +40,7 @@ export const helmValuesFileEnhancer = createEditorEnhancer(({state, resourceIden decorations.push(createInlineDecoration(placeUsed.locationInValueFile, InlineDecorationTypes.SatisfiedRef)); placeUsed.uses.forEach(use => { const newCommand = addEditorCommand({ + type: 'go_to_helm_template_file', text: `${use.filePath}`, altText: 'Select file', beforeText: 'Found in: ', @@ -63,6 +64,7 @@ export const helmValuesFileEnhancer = createEditorEnhancer(({state, resourceIden if (commands.length) { addEditorHover({ + type: 'helm_values_used_in_template', range: placeUsed.locationInValueFile, contents: commands.map(c => c.markdownLink), }); diff --git a/src/editor/enhancers/k8sResource/refs.ts b/src/editor/enhancers/k8sResource/refs.ts index 85b84b4c09..157143fe3d 100644 --- a/src/editor/enhancers/k8sResource/refs.ts +++ b/src/editor/enhancers/k8sResource/refs.ts @@ -168,6 +168,7 @@ ${outgoingRefsMarkdownTableRows.join('\n')} ); } addEditorHover({ + type: 'list_outgoing_links', range, contents: hoverContents, }); @@ -175,11 +176,12 @@ ${outgoingRefsMarkdownTableRows.join('\n')} if (matchedRefs.length === 1 && getEditorType() !== 'cluster') { const ref = matchedRefs[0]; addEditorLink({ + type: 'go_to_outgoing_link', range, handler: () => onClickRefLink({resourceMeta, ref, dispatch}), }); } else { - addEditorLink({range, handler: () => {}}); + addEditorLink({type: 'go_to_outgoing_link', range, handler: () => {}}); } }); }); @@ -196,6 +198,7 @@ const addEditorCommandForRef = (args: {resourceMeta: ResourceMeta; ref: Resource if (ref.target.type === 'resource' && ref.target.resourceId) { command = addEditorCommand( { + type: 'go_to_resource', text: 'Open resource', altText: 'Open resource', handler: () => { @@ -215,6 +218,7 @@ const addEditorCommandForRef = (args: {resourceMeta: ResourceMeta; ref: Resource } else if (ref.target.type === 'file') { command = addEditorCommand( { + type: 'go_to_file', text: `Open file`, altText: 'Open file', handler: () => { @@ -229,6 +233,7 @@ const addEditorCommandForRef = (args: {resourceMeta: ResourceMeta; ref: Resource } else if (ref.target.type === 'image') { command = addEditorCommand( { + type: 'go_to_image', text: `Open image`, altText: 'Open image', handler: () => { diff --git a/src/editor/enhancers/k8sResource/symbols.ts b/src/editor/enhancers/k8sResource/symbols.ts index fb8ac80eec..484b174029 100644 --- a/src/editor/enhancers/k8sResource/symbols.ts +++ b/src/editor/enhancers/k8sResource/symbols.ts @@ -43,6 +43,7 @@ function addNamespaceFilterLink( const namespace = getSymbolValue(lines, symbol); if (namespace) { const newCommand = addEditorCommand({ + type: 'filter_namespace', text: `Apply or remove`, altText: 'Add/remove namespace to/from current filter', handler: () => { @@ -57,6 +58,7 @@ function addNamespaceFilterLink( `); addEditorHover({ + type: 'list_namespace_filters', range: symbol.range, contents: [filterMarkdown], }); @@ -88,6 +90,7 @@ function addKindFilterLink( const kind = getSymbolValue(lines, symbol); if (kind) { const newCommand = addEditorCommand({ + type: 'filter_kind', text: `Apply or remove`, altText: 'Add/remove kind to/from current filter', handler: () => { @@ -102,6 +105,7 @@ function addKindFilterLink( `); addEditorHover({ + type: 'list_kind_filters', range: symbol.range, contents: [filterMarkdown], }); @@ -121,6 +125,7 @@ function addLabelFilterLink( const value = label.substring(symbol.name.length + 1).trim(); const newCommand = addEditorCommand({ + type: 'filter_label', text: `Apply or remove`, altText: 'Add/remove label to/from current filter', handler: () => { @@ -137,6 +142,7 @@ function addLabelFilterLink( `); addEditorHover({ + type: 'list_label_filters', range: symbol.range, contents: [filterMarkdown], }); @@ -156,6 +162,7 @@ function addAnnotationFilterLink( const value = annotation.substring(symbol.name.length + 1).trim(); const newCommand = addEditorCommand({ + type: 'filter_annotation', text: `${annotation}`, altText: 'Add/remove annotation to/from current filter', handler: () => { @@ -172,6 +179,7 @@ function addAnnotationFilterLink( `); addEditorHover({ + type: 'list_annotation_filters', range: symbol.range, contents: [filterMarkdown], }); @@ -190,6 +198,7 @@ function addDecodeSecretHover( const decoded = Buffer.from(value, 'base64').toString('utf-8'); const newCommand = addEditorCommand({ + type: 'secret_copy_to_clipboard', text: 'Copy to clipboard', altText: 'Copy decoded secret to clipboard', handler: () => { @@ -204,6 +213,7 @@ function addDecodeSecretHover( `); addEditorHover({ + type: 'decoded_secret', range: symbol.range, contents: [secretMarkdown], }); diff --git a/src/redux/services/clusterDashboard.ts b/src/redux/services/clusterDashboard.ts index 0c870f4b5f..1b3a1621d7 100644 --- a/src/redux/services/clusterDashboard.ts +++ b/src/redux/services/clusterDashboard.ts @@ -1,9 +1,16 @@ import * as k8s from '@kubernetes/client-node'; +import {uniq} from 'lodash'; + import {createKubeClientWithSetup} from '@redux/cluster/service/kube-client'; import {cpuParser, memoryParser} from '@utils/unit-converter'; +import {isDefined} from '@shared/utils/filter'; +import {trackEvent} from '@shared/utils/telemetry'; + +let lastContext: string | undefined; + export const getClusterUtilization = async (kubeconfig: string, context: string): Promise => { const kc = await createKubeClientWithSetup({context, kubeconfig, skipHealthCheck: true}); @@ -13,6 +20,28 @@ export const getClusterUtilization = async (kubeconfig: string, context: string) const nodeMetrics: k8s.NodeMetric[] = (await metricClient.getNodeMetrics()).items; const nodes = await k8s.topNodes(k8sApiClient); + let kubeletVersion: string | undefined; + + if (lastContext !== context) { + const providers = uniq( + nodes + .map(node => { + if (!kubeletVersion) { + kubeletVersion = node.Node.status?.nodeInfo?.kubeletVersion; + } + + const providerId = node.Node?.spec?.providerID; + // ID of the node assigned by the cloud provider in the format: :// + const providerParts = providerId?.split('://'); + if (providerParts?.length === 2) { + return providerParts[0]; + } + return undefined; + }) + .filter(isDefined) + ); + trackEvent('cluster/info', {providers, kubeletVersion}); + } return nodeMetrics.map(m => ({ nodeName: m.metadata.name, diff --git a/src/redux/thunks/project/createProject.ts b/src/redux/thunks/project/createProject.ts index c611649153..0f470db1d6 100644 --- a/src/redux/thunks/project/createProject.ts +++ b/src/redux/thunks/project/createProject.ts @@ -4,6 +4,7 @@ import {createProject} from '@redux/appConfig'; import {isFolderGitRepo} from '@redux/git/git.ipc'; import {Project} from '@shared/models/config'; +import {trackEvent} from '@shared/utils/telemetry'; import {setOpenProject} from './openProject'; @@ -18,4 +19,5 @@ export const setCreateProject = createAsyncThunk('config/setCreateProject', asyn thunkAPI.dispatch(createProject({...project, isGitRepo})); thunkAPI.dispatch(setOpenProject(project.rootFolder)); + trackEvent('app_start/create_project', {from: 'folder'}); }); diff --git a/src/redux/thunks/runPreviewConfiguration.ts b/src/redux/thunks/runPreviewConfiguration.ts index 7897831870..8c20dd6d5d 100644 --- a/src/redux/thunks/runPreviewConfiguration.ts +++ b/src/redux/thunks/runPreviewConfiguration.ts @@ -130,8 +130,8 @@ export const runPreviewConfiguration = createAsyncThunk< const result = await runCommandInMainThread(commandOptions); - if (result.error) { - trackEvent('preview/helm_config/fail', {reason: result.error}); + if (result.error || result.stderr) { + trackEvent('preview/helm_config/fail', {reason: result.error || result.stderr || 'unknown'}); return createRejectionWithAlert(thunkAPI, 'Helm Error', `${result.error} - ${result.stderr}`); } diff --git a/src/redux/validation/validation.listeners.tsx b/src/redux/validation/validation.listeners.tsx index 99416eeb5f..0a82be1981 100644 --- a/src/redux/validation/validation.listeners.tsx +++ b/src/redux/validation/validation.listeners.tsx @@ -47,6 +47,7 @@ import {doesSchemaExist} from '@utils/index'; import {ResourceIdentifier, ResourceStorage} from '@shared/models/k8sResource'; import {isDefined} from '@shared/utils/filter'; import {isEqual} from '@shared/utils/isEqual'; +import {trackEvent} from '@shared/utils/telemetry'; import {changeRuleLevel, setConfigK8sSchemaVersion, toggleRule, toggleValidation} from './validation.slice'; import {loadValidation, validateResources} from './validation.thunks'; @@ -73,6 +74,7 @@ const loadListener: AppListenerFn = listen => { changeRuleLevel ), async effect(_action, {dispatch, delay, signal, cancelActiveListeners}) { + trackEvent('validation/load_config', {actionType: _action.type}); if (isAnyOf(setIsInQuickClusterMode)(_action)) { if (!_action.payload) { return; @@ -114,6 +116,7 @@ const validateListener: AppListenerFn = listen => { ), async effect(_action, {dispatch, getState, cancelActiveListeners, signal, delay}) { cancelActiveListeners(); + trackEvent('validation/validate_all', {actionType: _action.type}); if (incrementalValidationStatus.isRunning) { incrementalValidationStatus.abortController?.abort(); @@ -178,6 +181,7 @@ const incrementalValidationListener: AppListenerFn = listen => { multiplePathsChanged.fulfilled ), async effect(_action, {dispatch, delay, signal}) { + trackEvent('validation/validate_incremental', {actionType: _action.type}); let resourceIdentifiers: ResourceIdentifier[] = []; if ( diff --git a/src/shared/models/telemetry.ts b/src/shared/models/telemetry.ts index 712aeb7506..5254655f65 100644 --- a/src/shared/models/telemetry.ts +++ b/src/shared/models/telemetry.ts @@ -46,7 +46,10 @@ export type EventMap = { numberOfValuesFiles: number; executionTime: number; }; - 'app_start/create_project': {from: 'scratch' | 'git' | 'template' | 'folder'; templateID?: string}; + 'app_start/create_project': { + from: 'sample' | 'scratch' | 'git' | 'template' | 'folder' | 'helm'; + templateID?: string; + }; 'app_start/select_page': {page: string}; 'app_start/select_project': undefined; 'project_list/open_project': undefined; @@ -76,13 +79,16 @@ export type EventMap = { 'explore/quick_search': undefined; 'graph/select_resource': {kind: string}; 'graph/select_image': undefined; - 'edit/code_changes': {from?: 'local' | 'cluster'}; + 'edit/code_changes': {from?: 'local' | 'cluster'; resourceKind?: string}; 'edit/template_use': {templateID: string}; 'edit/form_editor': {resourceKind?: string}; 'edit/source': {resourceKind?: string}; 'edit/side_by_side_editor': {resourceKind: string}; 'edit/select_hover_link': {type: 'resource' | 'image' | 'file'}; 'edit/graphview': {resourceKind?: string}; + 'editor/hover': {types?: string[]; resourceKind?: string}; + 'editor/follow_link': {types?: string[]; resourceKind?: string}; + 'editor/run_command': {type: string; resourceKind?: string}; 'create/file': undefined; 'create/folder': undefined; 'create/resource': {resourceKind: string}; @@ -117,6 +123,7 @@ export type EventMap = { 'cluster/actions/scale': {replicasNumber: number}; 'cluster/actions/restart': undefined; 'cluster/actions/delete': {kind: string}; + 'cluster/info': {kubeletVersion?: string; providers: string[]}; 'compare/opened': {from?: string}; 'compare/compared': {left?: string; right?: string; operation: string}; 'compare/inspected': {type?: string}; @@ -159,6 +166,9 @@ export type EventMap = { }; 'ai/generation/created-resources': {resourceKinds: string[]; resourcesCount: number}; 'logs/search': {resourceKind: string}; + 'validation/load_config': {actionType: string}; + 'validation/validate_all': {actionType: string}; + 'validation/validate_incremental': {actionType: string}; }; export const APP_INSTALLED = 'APP_INSTALLED';