mirror of
https://github.com/grafana/grafana.git
synced 2025-09-16 02:52:48 +08:00

* Bare-bones mocked integration * Create correct context based on the query * Add data source name * Do not bundle grafana/assistant with flame graph * Rename component * Add tests * Mock grafana/assistant * Update feature toggle and allow hiding the button * Update deps * Update types * Update yarn.lock * Fix typo in feature toggle description * Enable grafanaAssistantInProfilesDrilldown by default * Enable grafanaAssistantInProfilesDrilldown by default * Show Analyze Flame Graph button only if there's context for the assistant
343 lines
11 KiB
TypeScript
343 lines
11 KiB
TypeScript
import { createContext, ItemDataType } from '@grafana/assistant';
|
|
import {
|
|
DataFrame,
|
|
DataQueryResponse,
|
|
DataQueryRequest,
|
|
FieldType,
|
|
MutableDataFrame,
|
|
dateTime,
|
|
PreferredVisualisationType,
|
|
} from '@grafana/data';
|
|
|
|
import { GrafanaPyroscopeDataQuery } from './dataquery.gen';
|
|
import { enrichDataFrameWithAssistantContentMapper } from './utils';
|
|
|
|
// Mock the createContext function
|
|
jest.mock('@grafana/assistant', () => ({
|
|
createContext: jest.fn(),
|
|
ItemDataType: {
|
|
Datasource: 'datasource',
|
|
Structured: 'structured',
|
|
},
|
|
}));
|
|
|
|
const mockCreateContext = createContext as jest.MockedFunction<typeof createContext>;
|
|
|
|
describe('enrichDataFrameWithAssistantContentMapper', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
mockCreateContext.mockImplementation((type, data) => ({
|
|
type,
|
|
data,
|
|
node: { id: 'test-id', type: 'test', name: 'test-node', navigable: true },
|
|
occurrences: [],
|
|
}));
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
const createMockRequest = (
|
|
targets: Array<Partial<GrafanaPyroscopeDataQuery>> = []
|
|
): DataQueryRequest<GrafanaPyroscopeDataQuery> => ({
|
|
requestId: 'test-request',
|
|
interval: '1s',
|
|
intervalMs: 1000,
|
|
maxDataPoints: 1000,
|
|
range: {
|
|
from: dateTime('2023-01-01T00:00:00Z'),
|
|
to: dateTime('2023-01-01T01:00:00Z'),
|
|
raw: {
|
|
from: 'now-1h',
|
|
to: 'now',
|
|
},
|
|
},
|
|
scopedVars: {},
|
|
timezone: 'UTC',
|
|
app: 'explore',
|
|
startTime: Date.now(),
|
|
targets: targets.map((target, index) => ({
|
|
refId: `A${index}`,
|
|
profileTypeId: 'cpu',
|
|
labelSelector: '{service="test"}',
|
|
groupBy: [],
|
|
queryType: 'profile' as const,
|
|
datasource: {
|
|
uid: 'test-uid',
|
|
type: 'grafana-pyroscope-datasource',
|
|
},
|
|
...target,
|
|
})) as GrafanaPyroscopeDataQuery[],
|
|
});
|
|
|
|
const createMockDataFrame = (refId: string, preferredVisualisationType?: PreferredVisualisationType): DataFrame => {
|
|
const frame = new MutableDataFrame({
|
|
refId,
|
|
fields: [
|
|
{ name: 'time', type: FieldType.time, values: [1672531200000] },
|
|
{ name: 'value', type: FieldType.number, values: [100] },
|
|
],
|
|
});
|
|
|
|
if (preferredVisualisationType) {
|
|
frame.meta = {
|
|
preferredVisualisationType,
|
|
};
|
|
}
|
|
|
|
return frame;
|
|
};
|
|
|
|
const createMockResponse = (dataFrames: DataFrame[]): DataQueryResponse => ({
|
|
data: dataFrames,
|
|
});
|
|
|
|
describe('when processing flamegraph data frames', () => {
|
|
it('should enrich flamegraph data frame with assistant context', () => {
|
|
const request = createMockRequest([
|
|
{
|
|
refId: 'A0',
|
|
profileTypeId: 'cpu',
|
|
labelSelector: '{service="test"}',
|
|
},
|
|
]);
|
|
const dataFrame = createMockDataFrame('A0', 'flamegraph');
|
|
const response = createMockResponse([dataFrame]);
|
|
|
|
const mapper = enrichDataFrameWithAssistantContentMapper(request, 'PyroscopeDatasource');
|
|
const result = mapper(response);
|
|
|
|
expect(result.data).toHaveLength(1);
|
|
expect(result.data[0].meta?.custom?.assistantContext).toBeDefined();
|
|
expect(mockCreateContext).toHaveBeenCalledTimes(2);
|
|
|
|
// Verify datasource context
|
|
expect(mockCreateContext).toHaveBeenCalledWith(ItemDataType.Datasource, {
|
|
datasourceName: 'PyroscopeDatasource',
|
|
datasourceUid: 'test-uid',
|
|
datasourceType: 'grafana-pyroscope-datasource',
|
|
});
|
|
|
|
// Verify structured context
|
|
expect(mockCreateContext).toHaveBeenCalledWith(ItemDataType.Structured, {
|
|
title: 'Analyze Flame Graph',
|
|
data: {
|
|
start: request.range.from.valueOf(),
|
|
end: request.range.to.valueOf(),
|
|
profile_type_id: 'cpu',
|
|
label_selector: '{service="test"}',
|
|
operation: 'execute',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should preserve existing meta.custom properties', () => {
|
|
const request = createMockRequest([
|
|
{
|
|
refId: 'A0',
|
|
profileTypeId: 'memory',
|
|
labelSelector: '{app="backend"}',
|
|
},
|
|
]);
|
|
const dataFrame = createMockDataFrame('A0', 'flamegraph');
|
|
dataFrame.meta = {
|
|
preferredVisualisationType: 'flamegraph',
|
|
custom: {
|
|
existingProperty: 'value',
|
|
},
|
|
};
|
|
|
|
const response = createMockResponse([dataFrame]);
|
|
const mapper = enrichDataFrameWithAssistantContentMapper(request, 'TestDatasource');
|
|
const result = mapper(response);
|
|
|
|
expect(result.data[0].meta?.custom?.existingProperty).toBe('value');
|
|
expect(result.data[0].meta?.custom?.assistantContext).toBeDefined();
|
|
});
|
|
|
|
it('should handle multiple flamegraph data frames', () => {
|
|
const request = createMockRequest([
|
|
{
|
|
refId: 'A0',
|
|
profileTypeId: 'cpu',
|
|
labelSelector: '{service="test1"}',
|
|
},
|
|
{
|
|
refId: 'A1',
|
|
profileTypeId: 'memory',
|
|
labelSelector: '{service="test2"}',
|
|
},
|
|
]);
|
|
|
|
const dataFrames = [createMockDataFrame('A0', 'flamegraph'), createMockDataFrame('A1', 'flamegraph')];
|
|
const response = createMockResponse(dataFrames);
|
|
|
|
const mapper = enrichDataFrameWithAssistantContentMapper(request, 'TestDatasource');
|
|
const result = mapper(response);
|
|
|
|
expect(result.data).toHaveLength(2);
|
|
expect(result.data[0].meta?.custom?.assistantContext).toBeDefined();
|
|
expect(result.data[1].meta?.custom?.assistantContext).toBeDefined();
|
|
expect(mockCreateContext).toHaveBeenCalledTimes(4); // 2 contexts per frame
|
|
});
|
|
});
|
|
|
|
describe('when processing non-flamegraph data frames', () => {
|
|
it('should not modify data frames without flamegraph visualization type', () => {
|
|
const request = createMockRequest([
|
|
{
|
|
refId: 'A0',
|
|
profileTypeId: 'cpu',
|
|
labelSelector: '{service="test"}',
|
|
},
|
|
]);
|
|
const dataFrame = createMockDataFrame('A0', 'table');
|
|
const response = createMockResponse([dataFrame]);
|
|
|
|
const mapper = enrichDataFrameWithAssistantContentMapper(request, 'TestDatasource');
|
|
const result = mapper(response);
|
|
|
|
expect(result.data[0].meta?.custom?.assistantContext).toBeUndefined();
|
|
expect(mockCreateContext).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('when handling edge cases', () => {
|
|
it('should not add context data frame without matching query target', () => {
|
|
const request = createMockRequest([
|
|
{
|
|
refId: 'A0',
|
|
profileTypeId: 'cpu',
|
|
labelSelector: '{service="test"}',
|
|
},
|
|
]);
|
|
const dataFrame = createMockDataFrame('B0', 'flamegraph'); // Different refId
|
|
const response = createMockResponse([dataFrame]);
|
|
|
|
const mapper = enrichDataFrameWithAssistantContentMapper(request, 'TestDatasource');
|
|
const result = mapper(response);
|
|
|
|
expect(result.data[0]).toBe(dataFrame); // Should remain unchanged
|
|
expect(mockCreateContext).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not add context when query has no datasource information', () => {
|
|
const request = createMockRequest([
|
|
{
|
|
refId: 'A0',
|
|
profileTypeId: 'cpu',
|
|
labelSelector: '{service="test"}',
|
|
datasource: undefined,
|
|
},
|
|
]);
|
|
const dataFrame = createMockDataFrame('A0', 'flamegraph');
|
|
const response = createMockResponse([dataFrame]);
|
|
|
|
const mapper = enrichDataFrameWithAssistantContentMapper(request, 'TestDatasource');
|
|
const result = mapper(response);
|
|
|
|
expect(result.data[0]).toBe(dataFrame); // Should remain unchanged
|
|
expect(mockCreateContext).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not add context if query has incomplete datasource information', () => {
|
|
const request = createMockRequest([
|
|
{
|
|
refId: 'A0',
|
|
profileTypeId: 'cpu',
|
|
labelSelector: '{service="test"}',
|
|
datasource: {
|
|
uid: 'test-uid',
|
|
// Missing type
|
|
},
|
|
},
|
|
]);
|
|
const dataFrame = createMockDataFrame('A0', 'flamegraph');
|
|
const response = createMockResponse([dataFrame]);
|
|
|
|
const mapper = enrichDataFrameWithAssistantContentMapper(request, 'TestDatasource');
|
|
const result = mapper(response);
|
|
|
|
expect(result.data[0]).toBe(dataFrame); // Should remain unchanged
|
|
expect(mockCreateContext).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not add context with empty response data', () => {
|
|
const request = createMockRequest([]);
|
|
const response = createMockResponse([]);
|
|
|
|
const mapper = enrichDataFrameWithAssistantContentMapper(request, 'TestDatasource');
|
|
const result = mapper(response);
|
|
|
|
expect(result.data).toHaveLength(0);
|
|
expect(mockCreateContext).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle mixed data frame types', () => {
|
|
const request = createMockRequest([
|
|
{
|
|
refId: 'A0',
|
|
profileTypeId: 'cpu',
|
|
labelSelector: '{service="test"}',
|
|
},
|
|
{
|
|
refId: 'A1',
|
|
profileTypeId: 'memory',
|
|
labelSelector: '{service="test2"}',
|
|
},
|
|
]);
|
|
|
|
const dataFrames = [
|
|
createMockDataFrame('A0', 'flamegraph'),
|
|
createMockDataFrame('A1', 'table'),
|
|
createMockDataFrame('A0', 'flamegraph'), // Another flamegraph with same refId
|
|
];
|
|
const response = createMockResponse(dataFrames);
|
|
|
|
const mapper = enrichDataFrameWithAssistantContentMapper(request, 'TestDatasource');
|
|
const result = mapper(response);
|
|
|
|
expect(result.data).toHaveLength(3);
|
|
expect(result.data[0].meta?.custom?.assistantContext).toBeDefined(); // Flamegraph
|
|
expect(result.data[1].meta?.custom?.assistantContext).toBeUndefined(); // Table
|
|
expect(result.data[2].meta?.custom?.assistantContext).toBeDefined(); // Flamegraph
|
|
expect(mockCreateContext).toHaveBeenCalledTimes(4); // 2 contexts for each flamegraph
|
|
});
|
|
});
|
|
|
|
describe('context creation', () => {
|
|
it('should create context with correct time range values', () => {
|
|
const fromTime = dateTime('2023-06-15T10:00:00Z');
|
|
const toTime = dateTime('2023-06-15T11:00:00Z');
|
|
|
|
const request = createMockRequest([
|
|
{
|
|
refId: 'A0',
|
|
profileTypeId: 'cpu',
|
|
labelSelector: '{service="test"}',
|
|
},
|
|
]);
|
|
request.range.from = fromTime;
|
|
request.range.to = toTime;
|
|
|
|
const dataFrame = createMockDataFrame('A0', 'flamegraph');
|
|
const response = createMockResponse([dataFrame]);
|
|
|
|
const mapper = enrichDataFrameWithAssistantContentMapper(request, 'TestDatasource');
|
|
mapper(response);
|
|
|
|
expect(mockCreateContext).toHaveBeenCalledWith(ItemDataType.Structured, {
|
|
title: 'Analyze Flame Graph',
|
|
data: {
|
|
start: fromTime.valueOf(),
|
|
end: toTime.valueOf(),
|
|
profile_type_id: 'cpu',
|
|
label_selector: '{service="test"}',
|
|
operation: 'execute',
|
|
},
|
|
});
|
|
});
|
|
});
|
|
});
|