Tempo: TraceQL autocomplete feature tracking (#60876)

* Track usages of completion items in TraceQL query editor

* Change back traceID reportInteraction() name to avoid breaking dashboards

* Filter out TAG_VALUE labels from feature tracking to avoid exposing sensitive data
This commit is contained in:
Andre Pereira 2023-01-03 10:43:11 +00:00 committed by GitHub
parent 993ab2587e
commit 44afad2ce4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 42 additions and 12 deletions

View File

@ -181,7 +181,7 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
// Check whether this is a trace ID or traceQL query by checking if it only contains hex characters
if (queryValue.trim().match(hexOnlyRegex)) {
// There's only hex characters so let's assume that this is a trace ID
reportInteraction('grafana_traces_traceql_traceID_queried', {
reportInteraction('grafana_traces_traceID_queried', {
datasourceType: 'tempo',
app: options.app ?? '',
query: queryValue ?? '',

View File

@ -3,6 +3,7 @@ import type { languages } from 'monaco-editor';
import React, { useEffect, useRef } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { CodeEditor, Monaco, monacoTypes, useTheme2 } from '@grafana/ui';
import { createErrorNotification } from '../../../../core/copy/appNotification';
@ -10,7 +11,7 @@ import { notifyApp } from '../../../../core/reducers/appNotification';
import { dispatch } from '../../../../store/store';
import { TempoDatasource } from '../datasource';
import { CompletionProvider } from './autocomplete';
import { CompletionProvider, CompletionType } from './autocomplete';
import { languageDefinition } from './traceql';
interface Props {
@ -51,7 +52,7 @@ export function TraceQLEditor(props: Props) {
}}
onBeforeEditorMount={ensureTraceQL}
onEditorDidMount={(editor, monaco) => {
setupAutocompleteFn(editor, monaco);
setupAutocompleteFn(editor, monaco, setupRegisterInteractionCommand(editor));
setupActions(editor, monaco, onRunQuery);
setupPlaceholder(editor, monaco, styles);
setupAutoSize(editor);
@ -101,6 +102,17 @@ function setupActions(editor: monacoTypes.editor.IStandaloneCodeEditor, monaco:
});
}
function setupRegisterInteractionCommand(editor: monacoTypes.editor.IStandaloneCodeEditor): string | null {
return editor.addCommand(0, function (_, label, type: CompletionType) {
const properties: Record<string, unknown> = { datasourceType: 'tempo', type };
// Filter out the label for TAG_VALUE completions to avoid potentially exposing sensitive data
if (type !== 'TAG_VALUE') {
properties.label = label;
}
reportInteraction('grafana_traces_traceql_completion', properties);
});
}
function setupAutoSize(editor: monacoTypes.editor.IStandaloneCodeEditor) {
const container = editor.getDomNode();
const updateHeight = () => {
@ -156,9 +168,14 @@ function useAutocomplete(datasource: TempoDatasource) {
}, []);
// This should be run in monaco onEditorDidMount
return (editor: monacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco) => {
return (
editor: monacoTypes.editor.IStandaloneCodeEditor,
monaco: Monaco,
registerInteractionCommandId: string | null
) => {
providerRef.current.editor = editor;
providerRef.current.monaco = monaco;
providerRef.current.setRegisterInteractionCommandId(registerInteractionCommandId);
const { dispose } = monaco.languages.registerCompletionItemProvider(langId, providerRef.current);
autocompleteDisposeFun.current = dispose;

View File

@ -13,9 +13,11 @@ interface Props {
*/
export class CompletionProvider implements monacoTypes.languages.CompletionItemProvider {
languageProvider: TempoLanguageProvider;
registerInteractionCommandId: string | null;
constructor(props: Props) {
this.languageProvider = props.languageProvider;
this.registerInteractionCommandId = null;
}
triggerCharacters = ['{', '.', '[', '(', '=', '~', ' ', '"'];
@ -57,14 +59,19 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP
// so that monaco keeps the order we use
const maxIndexDigits = items.length.toString().length;
const suggestions: monacoTypes.languages.CompletionItem[] = items.map((item, index) => {
const suggestion = {
const suggestion: monacoTypes.languages.CompletionItem = {
kind: getMonacoCompletionItemKind(item.type, this.monaco!),
label: item.label,
insertText: item.insertText,
sortText: index.toString().padStart(maxIndexDigits, '0'), // to force the order we have
range,
command: {
id: this.registerInteractionCommandId || 'noOp',
title: 'Report Interaction',
arguments: [item.label, item.type],
},
};
fixSuggestion(suggestion, item.type, model, offset, this.monaco!);
fixSuggestion(suggestion, item.type, model, offset);
return suggestion;
});
return { suggestions };
@ -78,6 +85,13 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP
tags.forEach((t) => (this.tags[t] = new Set<string>()));
}
/**
* Set the ID for the registerInteraction command, to be used to keep track of how many completions are used by the users
*/
setRegisterInteractionCommandId(id: string | null) {
this.registerInteractionCommandId = id;
}
private overrideTagName(tagName: string): string {
switch (tagName) {
case 'status':
@ -88,7 +102,7 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP
}
private async getTagValues(tagName: string): Promise<Array<SelectableValue<string>>> {
let tagValues: Array<SelectableValue<string>> = [];
let tagValues: Array<SelectableValue<string>>;
if (this.cachedValues.hasOwnProperty(tagName)) {
tagValues = this.cachedValues[tagName];
@ -404,11 +418,10 @@ function getRangeAndOffset(monaco: Monaco, model: monacoTypes.editor.ITextModel,
* here.
*/
function fixSuggestion(
suggestion: monacoTypes.languages.CompletionItem & { range: monacoTypes.IRange },
suggestion: monacoTypes.languages.CompletionItem,
itemType: CompletionType,
model: monacoTypes.editor.ITextModel,
offset: number,
monaco: Monaco
offset: number
) {
if (itemType === 'TAG_NAME') {
const match = model
@ -427,10 +440,10 @@ function fixSuggestion(
}
// Adjust the range, so that we will replace the whole tag.
suggestion.range = monaco.Range.lift({
suggestion.range = {
...suggestion.range,
startColumn: offset - tag.length + 1,
});
};
}
}
}