mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-22 03:10:41 +01:00
Compare commits
2 Commits
nv/remove-
...
fix/ui-tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e35351799 | ||
|
|
cf211fdae4 |
@@ -63,6 +63,26 @@ const EDITED_SPAN_JSON = `{
|
||||
}
|
||||
}`;
|
||||
|
||||
const SPAN_WITH_EXTRA_KEY_JSON = `{
|
||||
"attributes": {
|
||||
"input.value": "What is quantum computing?"
|
||||
},
|
||||
"resource": {
|
||||
"service.name": "llm-gateway"
|
||||
},
|
||||
"demo": {
|
||||
"name": "demo"
|
||||
}
|
||||
}`;
|
||||
|
||||
const EXTRA_KEY_RESULT_SPAN = {
|
||||
attributes: {
|
||||
'input.value': 'What is quantum computing?',
|
||||
[MAPPED_ATTRIBUTE_KEY]: 'What is quantum computing?',
|
||||
},
|
||||
resource: { 'service.name': 'llm-gateway' },
|
||||
};
|
||||
|
||||
const SPAN_INPUT_KEY = LOCALSTORAGE.LLM_ATTRIBUTE_MAPPING_TEST_SPAN;
|
||||
|
||||
describe('TestTab — sample-span flow', () => {
|
||||
@@ -104,6 +124,47 @@ describe('TestTab — sample-span flow', () => {
|
||||
expect(screen.queryByTestId('test-error')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('trims extra top-level keys and sends only the envelope', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
let body: { spans?: { attributes?: Record<string, unknown> }[] } | undefined;
|
||||
server.use(
|
||||
rest.post(TEST_ENDPOINT, async (req, res, ctx) => {
|
||||
body = await req.json();
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(makeTestResponse([EXTRA_KEY_RESULT_SPAN])),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
render(<LLMObservabilityAttributeMapping />);
|
||||
|
||||
await user.click(screen.getByRole('tab', { name: 'Test' }));
|
||||
const runBtn = await screen.findByTestId('run-test-button');
|
||||
|
||||
await user.clear(screen.getByTestId('monaco'));
|
||||
await user.paste(SPAN_WITH_EXTRA_KEY_JSON);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('monaco')).toHaveValue(SPAN_WITH_EXTRA_KEY_JSON),
|
||||
);
|
||||
expect(screen.queryByTestId('test-input-error')).not.toBeInTheDocument();
|
||||
|
||||
await user.click(runBtn);
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('test-results'),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(body?.spans?.[0]?.attributes).toStrictEqual({
|
||||
'input.value': 'What is quantum computing?',
|
||||
});
|
||||
expect(screen.getByTestId('test-result-0-attributes')).toHaveTextContent(
|
||||
MAPPED_ATTRIBUTE_KEY,
|
||||
);
|
||||
expect(screen.getByTestId('test-result-0-resource')).toBeInTheDocument();
|
||||
expect(screen.getByText('populated')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('surfaces a backend error and renders no results', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
server.use(
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { parseSpanInput } from '../testPayload';
|
||||
|
||||
describe('parseSpanInput', () => {
|
||||
it('reads the envelope and trims extra top-level keys', () => {
|
||||
const span = parseSpanInput(`{
|
||||
"attributes": { "llm.model_name": "gpt-4o" },
|
||||
"resource": { "service.name": "llm-gateway" },
|
||||
"demo": { "name": "demo" }
|
||||
}`);
|
||||
|
||||
expect(span.attributes).toStrictEqual({ 'llm.model_name': 'gpt-4o' });
|
||||
expect(span.resource).toStrictEqual({ 'service.name': 'llm-gateway' });
|
||||
});
|
||||
|
||||
it('reads a clean envelope', () => {
|
||||
const span = parseSpanInput(`{
|
||||
"attributes": { "llm.model_name": "gpt-4o" },
|
||||
"resource": { "service.name": "llm-gateway" }
|
||||
}`);
|
||||
|
||||
expect(span.attributes).toStrictEqual({ 'llm.model_name': 'gpt-4o' });
|
||||
expect(span.resource).toStrictEqual({ 'service.name': 'llm-gateway' });
|
||||
});
|
||||
|
||||
it('treats an envelope-less object as a bare attribute map', () => {
|
||||
const span = parseSpanInput('{ "llm.model_name": "gpt-4o", "demo": "x" }');
|
||||
|
||||
expect(span.attributes).toStrictEqual({
|
||||
'llm.model_name': 'gpt-4o',
|
||||
demo: 'x',
|
||||
});
|
||||
expect(span.resource).toStrictEqual({});
|
||||
});
|
||||
|
||||
it('drops an envelope key that is not an object', () => {
|
||||
const span = parseSpanInput(
|
||||
'{ "attributes": { "llm.provider": "openai" }, "resource": "oops" }',
|
||||
);
|
||||
|
||||
expect(span.attributes).toStrictEqual({ 'llm.provider': 'openai' });
|
||||
expect(span.resource).toStrictEqual({});
|
||||
});
|
||||
|
||||
it.each([
|
||||
[' ', 'Paste a JSON span object to run the test.'],
|
||||
['{ "a": }', 'Invalid JSON — check for trailing commas or missing quotes.'],
|
||||
['[1, 2]', 'Span must be a JSON object of attribute key-value pairs.'],
|
||||
])('rejects %p', (input, message) => {
|
||||
expect(() => parseSpanInput(input)).toThrow(message);
|
||||
});
|
||||
});
|
||||
@@ -51,13 +51,9 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
// Any other top-level key (a real span carries name, spanId, kind...) is trimmed.
|
||||
function isSpanEnvelope(parsed: Record<string, unknown>): boolean {
|
||||
const keys = Object.keys(parsed);
|
||||
return (
|
||||
keys.length > 0 &&
|
||||
keys.every((key) => key === 'attributes' || key === 'resource') &&
|
||||
(isPlainObject(parsed.attributes) || isPlainObject(parsed.resource))
|
||||
);
|
||||
return isPlainObject(parsed.attributes) || isPlainObject(parsed.resource);
|
||||
}
|
||||
|
||||
export function parseSpanInput(input: string): SpantypesSpanMapperTestSpanDTO {
|
||||
|
||||
@@ -167,7 +167,7 @@ describe('UnpricedModelsTab (integration)', () => {
|
||||
|
||||
await screen.findByTestId(`unpriced-model-name-${MODEL}`);
|
||||
|
||||
// Open the row's dropdown and take the "Create pricing for …" escape hatch
|
||||
// Open the row's dropdown and take the "Create a new pricing model" escape hatch
|
||||
// instead of mapping onto an existing billing model.
|
||||
await user.click(screen.getByTestId(`map-to-select-${MODEL}`));
|
||||
await user.click(await screen.findByTestId(`map-to-create-${MODEL}`));
|
||||
|
||||
@@ -14,6 +14,24 @@
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.createFooter {
|
||||
padding: var(--spacing-2);
|
||||
background-color: var(--l2-background);
|
||||
}
|
||||
|
||||
.createItem {
|
||||
gap: var(--spacing-4);
|
||||
font-style: normal;
|
||||
color: var(--accent-primary);
|
||||
--command-item-cursor: pointer;
|
||||
--command-item-svg-size: var(--spacing-7);
|
||||
|
||||
&[data-selected='true'] {
|
||||
background-color: var(--callout-primary-background);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.skeletonList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -119,15 +119,18 @@ function MapToBillingModelSelect({
|
||||
options scroll. Escape hatch when no existing billing model fits:
|
||||
define this model's own pricing rather than mapping onto another. */}
|
||||
<ComboboxSeparator alwaysRender />
|
||||
<ComboboxCreateItem
|
||||
inputValue={modelName}
|
||||
value={`create-pricing-${modelName}`}
|
||||
prefix={<Plus size={14} />}
|
||||
onSelect={handleCreateNew}
|
||||
testId={`map-to-create-${modelName}`}
|
||||
>
|
||||
Create pricing for "{modelName}"
|
||||
</ComboboxCreateItem>
|
||||
<div className={styles.createFooter}>
|
||||
<ComboboxCreateItem
|
||||
className={styles.createItem}
|
||||
inputValue={modelName}
|
||||
value={`create-pricing-${modelName}`}
|
||||
prefix={<Plus size={14} />}
|
||||
onSelect={handleCreateNew}
|
||||
testId={`map-to-create-${modelName}`}
|
||||
>
|
||||
Create a new pricing model
|
||||
</ComboboxCreateItem>
|
||||
</div>
|
||||
</ComboboxCommand>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
|
||||
Reference in New Issue
Block a user