In this post, we will showcase how we at Tracetest built a custom front-end non-simple code editor based on React for the advanced query language created by the team to target spans across an OTEL Trace.
Introduction to Our Advanced Selector Query Language
Tracetest allows users to create assertions targeting specific spans from a trace simply and reliably.
A Trace can be composed by N number of spans that, at the same time, can be nested, repeated, or split into multiple queue producer/consumer sections.
Having a simple way to target the spans that matter for your assertions is instrumental to have good and reliable tests that will help you sleep at night.
To achieve this, Tracetest uses a custom span selector language to target spans. It inherits part of its syntax highlighting from CSS by using attribute matches, pseudo selectors, and child-parent operators.
This query language enables users to create the selectors the Tracetest system uses to apply the different assertions. Each section of the selector is composed of the following tokens:
- Span Wrapper. To start describing a query, a span wrapper is the first thing that needs to be added.
Example: span[].
- Attribute Matchers. Inside the span wrapper, a list of expressions can be added to narrow down the specific span that the user wants to match.
Example: name="get request" tracetest.span.type = "database".
- Pseudo Selectors. Sometimes you’ll need to be more specific around what span to choose from a collection and a pseudo selector similar to CSS can help with that.
Example: span[]:first.
- Span Operators. Currently, the query language we’ve built supports the child-parent (ancestor) operator and Or operator. You can write and combine both to match multiple spans or select one that is a child of a specific one.
Example: span[name="get request"] span[tracetest.span.type = "database"], span[http.status_code contains "200"]
Advanced Examples:
// selects all HTTP spans which status code is 200
span[tracetest.span.type = "http" http.status_code = "200"],
// selects the first database span which database name is pokeshop
span[tracetest.span.type = "database" db.name = "pokeshop"]:first,
// selects the second child of a span with name "POST /pokemon/import"
// which name is "validate request"
span[name = "POST /pokemon/import"] span[name = "validate request"]:nth_child(2)
For more information about advanced selectors and the query language, visit the Tracetest docs.
We Needed a Code Editor in Our UI
There are two main ways to use Tracetest.
The first is by using the CLI that can be fed by YAML text files allowing CI/CD processes and automation.
The second is by using the UI to create tests, execute requests and create assertions/checks. Tests created directly in YAML can be loaded into the UI and vice versa.
When using the YAML text files, creating advanced queries is pretty straightforward as you can define the different selectors as text to trigger the process.
The complexity comes when trying to implement the same level of functionality the language provides in the UI as there can be many branches, nested selectors, operators, etc.
The initial UI implementation only supported a single span wrapper with its attribute expressions and a separated input for pseudo selectors.
Another important issue is that if a user creates a selector that uses any of the language features that are not supported by the UI, then, when viewing it in the UI, the selector won't match the original text version. Trying to edit it will break the selector in most of the cases.
Creating the Non-simple Code Editor in React
At Tracetest, we knew we needed to let users build complex queries using some advanced query method in the near future. And with that in mind, we started thinking about the key features that we wanted to implement with the advanced editor.
They are:
- Syntax Highlighting
- Autocomplete
- Lint and Error Prompt
In order to achieve this, we started researching potential solutions and found Code Mirror.
Code Mirror is an all-in-one code editor for the web that can be extended to support multiple standard languages and themes like Javascript, Go, Ruby, etc. It also provides a simple way to build your own custom parsers and themes.
And that’s just what we needed in order to accomplish the key features - to come up with our own parser. For that, we used Lezer, which is a tool to create custom grammar rules by specifying the different tokens and expressions.
As you learned at the beginning of this post, we defined the query language tokens and, based on that, we started to mix them to create our expressions.
Things like:
BaseExpression { Identifier Operator ComparatorValue }
// or
SpanOrMatch { expression Comma !list expression }
Once we had the grammar ready, we could finalize the language setup using Typescript.
import {LRLanguage, LanguageSupport} from '@codemirror/language';
import {styleTags, tags as t} from '@lezer/highlight';
import {parser} from './grammar';
export const tracetestLang = LRLanguage.define({
parser: parser.configure({
props: [
styleTags({
Identifier: t.keyword,
String: t.string,
Operator: t.operatorKeyword,
Number: t.number,
Span: t.tagName,
ClosingBracket: t.tagName,
Comma: t.operatorKeyword,
PseudoSelector: t.operatorKeyword,
}),
],
}),
});
export const tracetest = () => {
return new LanguageSupport(tracetestLang);
};
Next, we had to come up with a way to support Autocomplete and Linting.
The Code Mirror framework includes two separate packages to handle Autocomplete and Linting, where you can add the different rules that, combined with the parser, can enable these features.
We created two custom hooks to handle this functionality that can be found here.
When complete, the editor React component looked like this:
import {useMemo} from 'react';
import {noop} from 'lodash';
import CodeMirror from '@uiw/react-codemirror';
import {autocompletion} from '@codemirror/autocomplete';
import {tracetest} from 'utils/grammar';
import {linter} from '@codemirror/lint';
import useAutoComplete from './hooks/useAutoComplete';
import useLint from './hooks/useLint';
import useEditorTheme from './hooks/useEditorTheme';
import * as S from './AdvancedEditor.styled';
interface IProps {
testId: string;
runId: string;
value?: string;
onChange?(value: string): void;
}
const AdvancedEditor = ({testId, runId, onChange = noop, value = ''}: IProps) => {
const completionFn = useAutoComplete({testId, runId});
const lintFn = useLint({testId, runId});
const editorTheme = useEditorTheme();
const extensionList = useMemo(
() => [autocompletion({override: [completionFn]}), linter(lintFn), tracetest()],
[completionFn, lintFn]
);
return (
<S.AdvancedEditor>
<CodeMirror
data-cy="advanced-selector"
value={value}
maxHeight="120px"
extensions={extensionList}
onChange={onChange}
spellCheck={false}
autoFocus
theme={editorTheme}
/>
</S.AdvancedEditor>
);
};
export default AdvancedEditor;
Final Code Editor Look, Feel and Functionality
From the UI perspective, this was the final result:
https://www.youtube.com/watch?v=tmpZeA3zJXE
The video showcases the final version of the advance editor. It includes a demo of the main features and how the React application switches the state of the Diagram when updating the code within the editor.
It also shows the invalid query state error message which validates if the input is valid before triggering the request to the backend.
At Tracetest, we always try to provide you the best user experience. No matter the platform (UI/CLI) we want to ensure you have the tools to interact with the system in an easy way.
Having said that, if you have any comments or suggestions feel free to join our Discord Community - we are always looking for feedback that can help improve Tracetest in any way.
Using OpenTelemetry tracing (you should be!) and want to give Tracetest a try? Download Tracetest from Github (MIT Licensed) and experience the latest in trace-based testing!
Top comments (0)