diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 976e09e5f..0aefb9412 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -17,14 +17,14 @@ jobs: # github.event.pull_request.user.login == 'external-contributor' || # github.event.pull_request.user.login == 'new-developer' || # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' - + runs-on: ubuntu-latest permissions: contents: read pull-requests: read issues: read id-token: write - + steps: - name: Checkout repository uses: actions/checkout@v4 @@ -46,12 +46,14 @@ jobs: - Performance considerations - Security concerns - Test coverage - + Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback. Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR. - + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md # or https://docs.claude.com/en/docs/claude-code/sdk#command-line for available options - claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"' - + claude_args: + '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh + issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr + view:*),Bash(gh pr list:*)"' diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index fcf23ea5d..a6ea7e396 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -35,7 +35,7 @@ jobs: uses: anthropics/claude-code-action@v1 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - + # This is an optional setting that allows Claude to read CI results on PRs additional_permissions: | actions: read @@ -47,4 +47,3 @@ jobs: # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md # or https://docs.claude.com/en/docs/claude-code/sdk#command-line for available options # claude_args: '--model claude-opus-4-1-20250805 --allowed-tools Bash(gh pr:*)' - diff --git a/packages/api/src/clickhouse/__tests__/renderChartConfig.test.ts b/packages/api/src/clickhouse/__tests__/renderChartConfig.test.ts index d99e12e5c..40b1aa15f 100644 --- a/packages/api/src/clickhouse/__tests__/renderChartConfig.test.ts +++ b/packages/api/src/clickhouse/__tests__/renderChartConfig.test.ts @@ -26,7 +26,7 @@ import { getServer, } from '@/fixtures'; import Connection from '@/models/connection'; -import { Source } from '@/models/source'; +import { LogSource, MetricSource } from '@/models/source'; const TEST_METRIC_TABLES = { sum: DEFAULT_METRICS_TABLE.SUM, @@ -77,7 +77,7 @@ describe('renderChartConfig', () => { username: config.CLICKHOUSE_USER, password: config.CLICKHOUSE_PASSWORD, }); - logSource = await Source.create({ + logSource = await LogSource.create({ kind: 'log', team: team._id, from: { @@ -88,7 +88,7 @@ describe('renderChartConfig', () => { connection: connection.id, name: 'Logs', }); - metricSource = await Source.create({ + metricSource = await MetricSource.create({ kind: 'metric', team: team._id, from: { diff --git a/packages/api/src/controllers/sources.ts b/packages/api/src/controllers/sources.ts index 791031603..2a238b947 100644 --- a/packages/api/src/controllers/sources.ts +++ b/packages/api/src/controllers/sources.ts @@ -1,4 +1,12 @@ -import { ISource, Source } from '@/models/source'; +import { SourceKind } from '@/../../common-utils/dist/types'; +import { + ISource, + LogSource, + MetricSource, + SessionSource, + Source, + TraceSource, +} from '@/models/source'; export function getSources(team: string) { return Source.find({ team }); @@ -9,7 +17,18 @@ export function getSource(team: string, sourceId: string) { } export function createSource(team: string, source: Omit) { - return Source.create({ ...source, team }); + switch (source.kind) { + case SourceKind.Log: + return LogSource.create({ ...source, team }); + case SourceKind.Trace: + return TraceSource.create({ ...source, team }); + case SourceKind.Metric: + return MetricSource.create({ ...source, team }); + case SourceKind.Session: + return SessionSource.create({ ...source, team }); + default: + throw new Error(`Source with kind ${source.kind} is invalid`); + } } export function updateSource( @@ -17,9 +36,26 @@ export function updateSource( sourceId: string, source: Omit, ) { - return Source.findOneAndUpdate({ _id: sourceId, team }, source, { - new: true, - }); + switch (source.kind) { + case SourceKind.Log: + return LogSource.findOneAndUpdate({ _id: sourceId, team }, source, { + new: true, + }); + case SourceKind.Trace: + return TraceSource.findOneAndUpdate({ _id: sourceId, team }, source, { + new: true, + }); + case SourceKind.Metric: + return MetricSource.findOneAndUpdate({ _id: sourceId, team }, source, { + new: true, + }); + case SourceKind.Session: + return SessionSource.findOneAndUpdate({ _id: sourceId, team }, source, { + new: true, + }); + default: + throw new Error(`Source with kind ${source.kind} is invalid`); + } } export function deleteSource(team: string, sourceId: string) { diff --git a/packages/api/src/models/source.ts b/packages/api/src/models/source.ts index 078e63b37..046a90890 100644 --- a/packages/api/src/models/source.ts +++ b/packages/api/src/models/source.ts @@ -1,86 +1,133 @@ import { + LogSourceSchema, MetricsDataType, + MetricSourceSchema, + SessionSourceSchema, SourceKind, + TraceSourceSchema, TSource, } from '@hyperdx/common-utils/dist/types'; import mongoose, { Schema } from 'mongoose'; +import { z } from 'zod'; -type ObjectId = mongoose.Types.ObjectId; - -export interface ISource extends Omit { - team: ObjectId; - connection: ObjectId | string; -} +import { objectIdSchema } from '@/utils/zod'; +const sourceExtension = { + team: objectIdSchema.or(z.instanceof(mongoose.Types.ObjectId)), + connection: objectIdSchema.or(z.instanceof(mongoose.Types.ObjectId)), +}; +const SourceModelSchema = z.discriminatedUnion('kind', [ + LogSourceSchema.extend(sourceExtension), + TraceSourceSchema.extend(sourceExtension), + SessionSourceSchema.extend(sourceExtension), + MetricSourceSchema.extend(sourceExtension), +]); +export type ISource = z.infer; export type SourceDocument = mongoose.HydratedDocument; export const Source = mongoose.model( 'Source', - new Schema( - { - kind: { - type: String, - enum: Object.values(SourceKind), - required: true, - }, - team: { - type: mongoose.Schema.Types.ObjectId, - required: true, - ref: 'Team', - }, - from: { - databaseName: String, - tableName: String, - }, - timestampValueExpression: String, - connection: { - type: mongoose.Schema.Types.ObjectId, - required: true, - ref: 'Connection', - }, - - name: String, - displayedTimestampValueExpression: String, - implicitColumnExpression: String, - serviceNameExpression: String, - bodyExpression: String, - tableFilterExpression: String, - eventAttributesExpression: String, - resourceAttributesExpression: String, - defaultTableSelectExpression: String, - uniqueRowIdExpression: String, - severityTextExpression: String, - traceIdExpression: String, - spanIdExpression: String, - traceSourceId: String, - sessionSourceId: String, - metricSourceId: String, + new Schema({ + name: String, + kind: { + type: String, + enum: Object.values(SourceKind), + required: true, + }, + from: { + databaseName: String, + tableName: String, + }, + team: { + type: mongoose.Schema.Types.ObjectId, + required: true, + ref: 'Team', + }, + connection: { + type: mongoose.Schema.Types.ObjectId, + required: true, + ref: 'Connection', + }, + }), +); - durationExpression: String, - durationPrecision: Number, - parentSpanIdExpression: String, - spanNameExpression: String, +export const LogSource = Source.discriminator< + Extract +>( + SourceKind.Log, + new mongoose.Schema>({ + timestampValueExpression: String, + defaultTableSelectExpression: String, + serviceNameExpression: String, + severityTextExpression: String, + bodyExpression: String, + eventAttributesExpression: String, + resourceAttributesExpression: String, + displayedTimestampValueExpression: String, + metricSourceId: String, + traceSourceId: String, + traceIdExpression: String, + spanIdExpression: String, + implicitColumnExpression: String, + uniqueRowIdExpression: String, + tableFilterExpression: String, + }), +); - logSourceId: String, - spanKindExpression: String, - statusCodeExpression: String, - statusMessageExpression: String, - spanEventsValueExpression: String, +export const TraceSource = Source.discriminator< + Extract +>( + SourceKind.Trace, + new mongoose.Schema>({ + defaultTableSelectExpression: String, + timestampValueExpression: String, + durationExpression: String, + durationPrecision: Number, + traceIdExpression: String, + spanIdExpression: String, + parentSpanIdExpression: String, + spanNameExpression: String, + spanKindExpression: String, + logSourceId: String, + sessionSourceId: String, + metricSourceId: String, + statusCodeExpression: String, + statusMessageExpression: String, + serviceNameExpression: String, + resourceAttributesExpression: String, + eventAttributesExpression: String, + spanEventsValueExpression: String, + implicitColumnExpression: String, + }), +); - metricTables: { - type: { - [MetricsDataType.Gauge]: String, - [MetricsDataType.Histogram]: String, - [MetricsDataType.Sum]: String, - [MetricsDataType.Summary]: String, - [MetricsDataType.ExponentialHistogram]: String, - }, - default: undefined, +export const MetricSource = Source.discriminator< + Extract +>( + SourceKind.Metric, + new mongoose.Schema>({ + metricTables: { + type: { + [MetricsDataType.Gauge]: String, + [MetricsDataType.Histogram]: String, + [MetricsDataType.Sum]: String, + [MetricsDataType.Summary]: String, + [MetricsDataType.ExponentialHistogram]: String, }, + default: undefined, }, - { - toJSON: { virtuals: true }, - timestamps: true, - }, - ), + timestampValueExpression: String, + resourceAttributesExpression: String, + logSourceId: String, + }), +); + +export const SessionSource = Source.discriminator< + Extract +>( + SourceKind.Session, + new mongoose.Schema>({ + traceSourceId: String, + timestampValueExpression: String, + }), ); diff --git a/packages/api/src/routers/api/__tests__/sources.test.ts b/packages/api/src/routers/api/__tests__/sources.test.ts index 871aa6558..371d2915c 100644 --- a/packages/api/src/routers/api/__tests__/sources.test.ts +++ b/packages/api/src/routers/api/__tests__/sources.test.ts @@ -1,10 +1,18 @@ -import { SourceKind, TSourceUnion } from '@hyperdx/common-utils/dist/types'; +import { SourceKind, TSource } from '@hyperdx/common-utils/dist/types'; import { Types } from 'mongoose'; +import { + TLogSource, + TMetricSource, + TTraceSource, +} from '@/../../common-utils/dist/types'; import { getLoggedInAgent, getServer } from '@/fixtures'; -import { Source } from '@/models/source'; +import Connection from '@/models/connection'; +import { LogSource, Source } from '@/models/source'; +import Team from '@/models/team'; +import { setupTeamDefaults } from '@/setupDefaults'; -const MOCK_SOURCE: Omit, 'id'> = { +const MOCK_SOURCE: Omit, 'id'> = { kind: SourceKind.Log, name: 'Test Source', connection: new Types.ObjectId().toString(), @@ -35,7 +43,7 @@ describe('sources router', () => { const { agent, team } = await getLoggedInAgent(server); // Create test source - await Source.create({ + await LogSource.create({ ...MOCK_SOURCE, team: team._id, }); @@ -93,7 +101,7 @@ describe('sources router', () => { const { agent, team } = await getLoggedInAgent(server); // Create test source - const source = await Source.create({ + const source = await LogSource.create({ ...MOCK_SOURCE, team: team._id, }); @@ -129,7 +137,7 @@ describe('sources router', () => { const { agent, team } = await getLoggedInAgent(server); // Create test source - const source = await Source.create({ + const source = await LogSource.create({ ...MOCK_SOURCE, team: team._id, }); @@ -150,3 +158,667 @@ describe('sources router', () => { await agent.delete(`/sources/${nonExistentId}`).expect(200); }); }); + +describe('sources router - comprehensive source type tests', () => { + const server = getServer(); + const connectionId = new Types.ObjectId().toString(); + + beforeAll(async () => { + await server.start(); + }); + + afterEach(async () => { + await server.clearDBs(); + }); + + afterAll(async () => { + await server.stop(); + }); + + describe('Log Source - Full Field Verification', () => { + it('POST / and GET / - creates and fetches log source with exact field matching', async () => { + const { agent, team } = await getLoggedInAgent(server); + + const mockLogSource: Omit = { + from: { + databaseName: 'default', + tableName: 'otel_logs', + }, + kind: SourceKind.Log, + timestampValueExpression: 'TimestampTime', + connection: connectionId, + name: 'Logs', + displayedTimestampValueExpression: 'Timestamp', + implicitColumnExpression: 'Body', + serviceNameExpression: 'ServiceName', + bodyExpression: 'Body', + eventAttributesExpression: 'LogAttributes', + resourceAttributesExpression: 'ResourceAttributes', + defaultTableSelectExpression: 'Timestamp,ServiceName,SeverityText,Body', + severityTextExpression: 'SeverityText', + traceIdExpression: 'TraceId', + spanIdExpression: 'SpanId', + metricSourceId: 'm', + traceSourceId: 's', + }; + + // Create the source + const createResponse = await agent + .post('/sources') + .send(mockLogSource) + .expect(200); + + // Verify creation response + expect(createResponse.body.kind).toBe(SourceKind.Log); + expect(createResponse.body.team).toBe(team._id.toString()); + + // Get all sources + const getResponse = await agent.get('/sources').expect(200); + expect(getResponse.body).toHaveLength(1); + + const fetchedSource = getResponse.body[0]; + + // Verify all fields match exactly + expect(fetchedSource.kind).toBe(SourceKind.Log); + expect(fetchedSource.name).toBe('Logs'); + expect(fetchedSource.connection).toBe(connectionId); + expect(fetchedSource.from).toEqual({ + databaseName: 'default', + tableName: 'otel_logs', + }); + expect(fetchedSource.timestampValueExpression).toBe('TimestampTime'); + expect(fetchedSource.displayedTimestampValueExpression).toBe('Timestamp'); + expect(fetchedSource.implicitColumnExpression).toBe('Body'); + expect(fetchedSource.serviceNameExpression).toBe('ServiceName'); + expect(fetchedSource.bodyExpression).toBe('Body'); + expect(fetchedSource.eventAttributesExpression).toBe('LogAttributes'); + expect(fetchedSource.resourceAttributesExpression).toBe( + 'ResourceAttributes', + ); + expect(fetchedSource.defaultTableSelectExpression).toBe( + 'Timestamp,ServiceName,SeverityText,Body', + ); + expect(fetchedSource.severityTextExpression).toBe('SeverityText'); + expect(fetchedSource.traceIdExpression).toBe('TraceId'); + expect(fetchedSource.spanIdExpression).toBe('SpanId'); + + // Verify auto-generated cross-reference IDs exist + expect(fetchedSource.metricSourceId).toBeTruthy(); + expect(fetchedSource.traceSourceId).toBeTruthy(); + }); + }); + + describe('Trace Source - Full Field Verification', () => { + it('POST / and GET / - creates and fetches trace source with exact field matching', async () => { + const { agent, team } = await getLoggedInAgent(server); + + const mockTraceSource: Omit = { + kind: SourceKind.Trace, + name: 'Traces', + connection: connectionId, + from: { + databaseName: 'default', + tableName: 'otel_traces', + }, + timestampValueExpression: 'Timestamp', + implicitColumnExpression: 'SpanName', + serviceNameExpression: 'ServiceName', + eventAttributesExpression: 'SpanAttributes', + resourceAttributesExpression: 'ResourceAttributes', + defaultTableSelectExpression: + 'Timestamp,ServiceName,StatusCode,round(Duration/1e6),SpanName', + traceIdExpression: 'TraceId', + spanIdExpression: 'SpanId', + durationExpression: 'Duration', + durationPrecision: 9, + parentSpanIdExpression: 'ParentSpanId', + spanNameExpression: 'SpanName', + spanKindExpression: 'SpanKind', + statusCodeExpression: 'StatusCode', + statusMessageExpression: 'StatusMessage', + logSourceId: 'l', + sessionSourceId: 's', + metricSourceId: 'm', + }; + + // Create the source + const createResponse = await agent + .post('/sources') + .send(mockTraceSource) + .expect(200); + + // Verify creation response + expect(createResponse.body.kind).toBe(SourceKind.Trace); + expect(createResponse.body.team).toBe(team._id.toString()); + + // Get all sources + const getResponse = await agent.get('/sources').expect(200); + expect(getResponse.body).toHaveLength(1); + + const fetchedSource = getResponse.body[0]; + + // Verify all fields match exactly + expect(fetchedSource.kind).toBe(SourceKind.Trace); + expect(fetchedSource.name).toBe('Traces'); + expect(fetchedSource.connection).toBe(connectionId); + expect(fetchedSource.from).toEqual({ + databaseName: 'default', + tableName: 'otel_traces', + }); + expect(fetchedSource.timestampValueExpression).toBe('Timestamp'); + expect(fetchedSource.implicitColumnExpression).toBe('SpanName'); + expect(fetchedSource.serviceNameExpression).toBe('ServiceName'); + expect(fetchedSource.eventAttributesExpression).toBe('SpanAttributes'); + expect(fetchedSource.resourceAttributesExpression).toBe( + 'ResourceAttributes', + ); + expect(fetchedSource.defaultTableSelectExpression).toBe( + 'Timestamp,ServiceName,StatusCode,round(Duration/1e6),SpanName', + ); + expect(fetchedSource.traceIdExpression).toBe('TraceId'); + expect(fetchedSource.spanIdExpression).toBe('SpanId'); + expect(fetchedSource.durationExpression).toBe('Duration'); + expect(fetchedSource.durationPrecision).toBe(9); + expect(fetchedSource.parentSpanIdExpression).toBe('ParentSpanId'); + expect(fetchedSource.spanNameExpression).toBe('SpanName'); + expect(fetchedSource.spanKindExpression).toBe('SpanKind'); + expect(fetchedSource.statusCodeExpression).toBe('StatusCode'); + expect(fetchedSource.statusMessageExpression).toBe('StatusMessage'); + + // Verify auto-generated cross-reference IDs exist + expect(fetchedSource.logSourceId).toBeTruthy(); + expect(fetchedSource.metricSourceId).toBeTruthy(); + expect(fetchedSource.sessionSourceId).toBeTruthy(); + }); + }); + + describe('Metric Source - Full Field Verification', () => { + it('POST / and GET / - creates and fetches metric source with exact field matching', async () => { + const { agent, team } = await getLoggedInAgent(server); + + const mockMetricSource: Omit = { + kind: SourceKind.Metric, + name: 'Metrics', + connection: connectionId, + from: { + databaseName: 'default', + tableName: '', + }, + timestampValueExpression: 'TimeUnix', + resourceAttributesExpression: 'ResourceAttributes', + metricTables: { + gauge: 'otel_metrics_gauge', + histogram: 'otel_metrics_histogram', + sum: 'otel_metrics_sum', + summary: 'otel_metrics_summary', + 'exponential histogram': 'otel_metrics_exponential_histogram', + }, + logSourceId: 'l', + }; + + // Create the source + const createResponse = await agent + .post('/sources') + .send(mockMetricSource) + .expect(200); + + // Verify creation response + expect(createResponse.body.kind).toBe(SourceKind.Metric); + expect(createResponse.body.team).toBe(team._id.toString()); + + // Get all sources + const getResponse = await agent.get('/sources').expect(200); + expect(getResponse.body).toHaveLength(1); + + const fetchedSource = getResponse.body[0]; + + // Verify all fields match exactly + expect(fetchedSource.kind).toBe(SourceKind.Metric); + expect(fetchedSource.name).toBe('Metrics'); + expect(fetchedSource.connection).toBe(connectionId); + expect(fetchedSource.from).toEqual({ + databaseName: 'default', + tableName: '', + }); + expect(fetchedSource.timestampValueExpression).toBe('TimeUnix'); + expect(fetchedSource.resourceAttributesExpression).toBe( + 'ResourceAttributes', + ); + expect(fetchedSource.metricTables).toMatchObject({ + gauge: 'otel_metrics_gauge', + histogram: 'otel_metrics_histogram', + sum: 'otel_metrics_sum', + summary: 'otel_metrics_summary', + 'exponential histogram': 'otel_metrics_exponential_histogram', + id: fetchedSource.metricTables.id, + }); + + // Verify auto-generated cross-reference IDs exist + expect(fetchedSource.logSourceId).toBeTruthy(); + }); + }); + + describe('Session Source - Full Field Verification', () => { + it('POST / and GET / - creates and fetches session source with exact field matching', async () => { + const { agent, team } = await getLoggedInAgent(server); + + // Based on the provided API response, session sources seem to have log-like fields + const mockSessionSource = { + kind: SourceKind.Session, + name: 'Sessions', + connection: connectionId, + from: { + databaseName: 'default', + tableName: 'hyperdx_sessions', + }, + traceSourceId: 't', + timestampValueExpression: 'TimestampTime', + }; + + // Create the source + const createResponse = await agent + .post('/sources') + .send(mockSessionSource) + .expect(200); + + // Verify creation response + expect(createResponse.body.kind).toBe(SourceKind.Session); + expect(createResponse.body.team).toBe(team._id.toString()); + + // Get all sources + const getResponse = await agent.get('/sources').expect(200); + expect(getResponse.body).toHaveLength(1); + + // Find the session source + const fetchedSource = getResponse.body.find( + (s: any) => s.kind === SourceKind.Session, + ); + + // Verify all fields match exactly + expect(fetchedSource.kind).toBe(SourceKind.Session); + expect(fetchedSource.name).toBe('Sessions'); + expect(fetchedSource.connection).toBe(connectionId); + expect(fetchedSource.from).toEqual({ + databaseName: 'default', + tableName: 'hyperdx_sessions', + }); + expect(fetchedSource.traceSourceId).toBeTruthy(); + + // Session sources inherit log-like fields + expect(fetchedSource.timestampValueExpression).toBe('TimestampTime'); + + // Verify auto-generated cross-reference IDs exist + expect(fetchedSource.traceSourceId).toBeTruthy(); + }); + }); + + describe('Multiple Source Creation and Retrieval', () => { + it('creates all source types and retrieves them with exact field matching', async () => { + const { agent, team } = await getLoggedInAgent(server); + + // Create log source + await agent + .post('/sources') + .send({ + kind: SourceKind.Log, + name: 'Logs', + connection: connectionId, + from: { + databaseName: 'default', + tableName: 'otel_logs', + }, + timestampValueExpression: 'TimestampTime', + defaultTableSelectExpression: + 'Timestamp,ServiceName,SeverityText,Body', + }) + .expect(200); + + // Create trace source + const traceResponse = await agent + .post('/sources') + .send({ + kind: SourceKind.Trace, + name: 'Traces', + connection: connectionId, + from: { + databaseName: 'default', + tableName: 'otel_traces', + }, + timestampValueExpression: 'Timestamp', + traceIdExpression: 'TraceId', + spanIdExpression: 'SpanId', + durationExpression: 'Duration', + durationPrecision: 9, + parentSpanIdExpression: 'ParentSpanId', + spanNameExpression: 'SpanName', + spanKindExpression: 'SpanKind', + }) + .expect(200); + + // Create metric source + await agent + .post('/sources') + .send({ + kind: SourceKind.Metric, + name: 'Metrics', + connection: connectionId, + from: { + databaseName: 'default', + tableName: '', + }, + timestampValueExpression: 'TimeUnix', + resourceAttributesExpression: 'ResourceAttributes', + metricTables: { + gauge: 'otel_metrics_gauge', + histogram: 'otel_metrics_histogram', + sum: 'otel_metrics_sum', + }, + }) + .expect(200); + + // Create session source + await agent + .post('/sources') + .send({ + kind: SourceKind.Session, + name: 'Sessions', + connection: connectionId, + from: { + databaseName: 'default', + tableName: 'hyperdx_sessions', + }, + traceSourceId: traceResponse.body._id, + }) + .expect(200); + + // Get all sources + const getResponse = await agent.get('/sources').expect(200); + expect(getResponse.body).toHaveLength(4); + + // Verify we have one of each type + const kinds = getResponse.body.map((s: any) => s.kind); + expect(kinds).toContain(SourceKind.Log); + expect(kinds).toContain(SourceKind.Trace); + expect(kinds).toContain(SourceKind.Metric); + expect(kinds).toContain(SourceKind.Session); + + // Verify each source has the expected structure + getResponse.body.forEach((source: any) => { + expect(source._id).toBeTruthy(); + expect(source.kind).toBeTruthy(); + expect(source.team).toBe(team._id.toString()); + expect(source.name).toBeTruthy(); + expect(source.connection).toBe(connectionId); + expect(source.from).toBeTruthy(); + expect(source.id).toBeTruthy(); + }); + }); + }); +}); + +// Mock the config module before importing setupTeamDefaults +jest.mock('@/config', () => { + const actualConfig = jest.requireActual('@/config'); + return { + ...actualConfig, + get DEFAULT_CONNECTIONS() { + return process.env.DEFAULT_CONNECTIONS; + }, + get DEFAULT_SOURCES() { + return process.env.DEFAULT_SOURCES; + }, + }; +}); + +describe('setupTeamDefaults', () => { + const server = getServer(); + + beforeAll(async () => { + await server.start(); + }); + + afterEach(async () => { + await server.clearDBs(); + jest.clearAllMocks(); + // Clear the environment variables after each test + delete process.env.DEFAULT_CONNECTIONS; + delete process.env.DEFAULT_SOURCES; + }); + + afterAll(async () => { + await server.stop(); + }); + + it('should create default sources with exact field matching', async () => { + // Define the default configurations + const defaultConnections = [ + { + name: 'Local ClickHouse', + host: 'http://localhost:8123', + username: 'default', + password: '', + }, + ]; + + const defaultSources = [ + { + from: { + databaseName: 'default', + tableName: 'otel_logs', + }, + kind: 'log', + timestampValueExpression: 'TimestampTime', + name: 'Logs', + displayedTimestampValueExpression: 'Timestamp', + implicitColumnExpression: 'Body', + serviceNameExpression: 'ServiceName', + bodyExpression: 'Body', + eventAttributesExpression: 'LogAttributes', + resourceAttributesExpression: 'ResourceAttributes', + defaultTableSelectExpression: 'Timestamp,ServiceName,SeverityText,Body', + severityTextExpression: 'SeverityText', + traceIdExpression: 'TraceId', + spanIdExpression: 'SpanId', + connection: 'Local ClickHouse', + traceSourceId: 'Traces', + sessionSourceId: 'Sessions', + metricSourceId: 'Metrics', + }, + { + from: { + databaseName: 'default', + tableName: 'otel_traces', + }, + kind: 'trace', + timestampValueExpression: 'Timestamp', + name: 'Traces', + displayedTimestampValueExpression: 'Timestamp', + implicitColumnExpression: 'SpanName', + serviceNameExpression: 'ServiceName', + bodyExpression: 'SpanName', + eventAttributesExpression: 'SpanAttributes', + resourceAttributesExpression: 'ResourceAttributes', + defaultTableSelectExpression: + 'Timestamp,ServiceName,StatusCode,round(Duration/1e6),SpanName', + traceIdExpression: 'TraceId', + spanIdExpression: 'SpanId', + durationExpression: 'Duration', + durationPrecision: 9, + parentSpanIdExpression: 'ParentSpanId', + spanNameExpression: 'SpanName', + spanKindExpression: 'SpanKind', + statusCodeExpression: 'StatusCode', + statusMessageExpression: 'StatusMessage', + connection: 'Local ClickHouse', + logSourceId: 'Logs', + sessionSourceId: 'Sessions', + metricSourceId: 'Metrics', + }, + { + from: { + databaseName: 'default', + tableName: '', + }, + kind: 'metric', + timestampValueExpression: 'TimeUnix', + name: 'Metrics', + resourceAttributesExpression: 'ResourceAttributes', + metricTables: { + gauge: 'otel_metrics_gauge', + histogram: 'otel_metrics_histogram', + sum: 'otel_metrics_sum', + }, + connection: 'Local ClickHouse', + logSourceId: 'Logs', + traceSourceId: 'Traces', + sessionSourceId: 'Sessions', + }, + { + name: 'Sessions', + kind: 'session', + from: { + databaseName: 'default', + tableName: 'hyperdx_sessions', + }, + connection: 'Local ClickHouse', + timestampValueExpression: 'TimestampTime', + traceSourceId: 'Traces', + }, + ]; + + // Set environment variables for this specific test + process.env.DEFAULT_CONNECTIONS = JSON.stringify(defaultConnections); + process.env.DEFAULT_SOURCES = JSON.stringify(defaultSources); + + // Create a team + const team = await Team.create({ + name: 'Test Team', + }); + + // Call setupTeamDefaults directly - it will read from env vars through our mock + await setupTeamDefaults(team._id.toString()); + + // Verify connections were created + const connections = await Connection.find({ team: team._id }); + expect(connections).toHaveLength(1); + expect(connections[0].name).toBe('Local ClickHouse'); + + // Verify sources were created + const sources = await Source.find({ team: team._id }); + expect(sources).toHaveLength(4); + + // Get each source type + const logSource = sources.find(s => s.kind === 'log'); + const traceSource = sources.find(s => s.kind === 'trace'); + const metricSource = sources.find(s => s.kind === 'metric'); + const sessionSource = sources.find(s => s.kind === 'session'); + + // Verify all source types were found + expect(logSource).toBeTruthy(); + if (!logSource || logSource.kind !== SourceKind.Log) return; + expect(traceSource).toBeTruthy(); + if (!traceSource || traceSource.kind !== SourceKind.Trace) return; + expect(metricSource).toBeTruthy(); + if (!metricSource || metricSource.kind !== SourceKind.Metric) return; + expect(sessionSource).toBeTruthy(); + if (!sessionSource || sessionSource.kind !== SourceKind.Session) return; + + // Verify LOG source has all expected fields + expect(logSource).toMatchObject({ + kind: 'log', + name: 'Logs', + from: { + databaseName: 'default', + tableName: 'otel_logs', + }, + timestampValueExpression: 'TimestampTime', + displayedTimestampValueExpression: 'Timestamp', + implicitColumnExpression: 'Body', + serviceNameExpression: 'ServiceName', + bodyExpression: 'Body', + eventAttributesExpression: 'LogAttributes', + resourceAttributesExpression: 'ResourceAttributes', + defaultTableSelectExpression: 'Timestamp,ServiceName,SeverityText,Body', + severityTextExpression: 'SeverityText', + traceIdExpression: 'TraceId', + spanIdExpression: 'SpanId', + }); + // Verify cross-references + expect(logSource.traceSourceId?.toString()).toBe( + traceSource!._id.toString(), + ); + expect(logSource.metricSourceId?.toString()).toBe( + metricSource!._id.toString(), + ); + + // Verify TRACE source has all expected fields + expect(traceSource).toMatchObject({ + kind: 'trace', + name: 'Traces', + from: { + databaseName: 'default', + tableName: 'otel_traces', + }, + timestampValueExpression: 'Timestamp', + implicitColumnExpression: 'SpanName', + serviceNameExpression: 'ServiceName', + eventAttributesExpression: 'SpanAttributes', + resourceAttributesExpression: 'ResourceAttributes', + defaultTableSelectExpression: + 'Timestamp,ServiceName,StatusCode,round(Duration/1e6),SpanName', + traceIdExpression: 'TraceId', + spanIdExpression: 'SpanId', + durationExpression: 'Duration', + durationPrecision: 9, + parentSpanIdExpression: 'ParentSpanId', + spanNameExpression: 'SpanName', + spanKindExpression: 'SpanKind', + statusCodeExpression: 'StatusCode', + statusMessageExpression: 'StatusMessage', + }); + // Verify cross-references + expect(traceSource.logSourceId?.toString()).toBe(logSource!._id.toString()); + expect(traceSource.sessionSourceId?.toString()).toBe( + sessionSource!._id.toString(), + ); + expect(traceSource.metricSourceId?.toString()).toBe( + metricSource!._id.toString(), + ); + + // Verify METRIC source has all expected fields + expect(metricSource).toMatchObject({ + kind: 'metric', + name: 'Metrics', + from: { + databaseName: 'default', + tableName: '', + }, + timestampValueExpression: 'TimeUnix', + resourceAttributesExpression: 'ResourceAttributes', + metricTables: { + gauge: 'otel_metrics_gauge', + histogram: 'otel_metrics_histogram', + sum: 'otel_metrics_sum', + }, + }); + // Verify cross-references + expect(metricSource.logSourceId?.toString()).toBe( + logSource!._id.toString(), + ); + + // Verify SESSION source has all expected fields + expect(sessionSource).toMatchObject({ + kind: 'session', + name: 'Sessions', + from: { + databaseName: 'default', + tableName: 'hyperdx_sessions', + }, + timestampValueExpression: 'TimestampTime', + }); + // Verify cross-references + expect(sessionSource.traceSourceId.toString()).toBe( + traceSource!._id.toString(), + ); + }); +}); diff --git a/packages/api/src/routers/api/ai.ts b/packages/api/src/routers/api/ai.ts index 096b110a2..a860d2ea7 100644 --- a/packages/api/src/routers/api/ai.ts +++ b/packages/api/src/routers/api/ai.ts @@ -268,7 +268,7 @@ router.post( connection: connectionId, where: '', groupBy: '', - timestampValueExpression: source.timestampValueExpression, + timestampValueExpression: source.timestampValueExpression ?? '', dateRange: [new Date(Date.now() - ms('60m')), new Date()], }; const keyValues = await metadata.getKeyValues({ @@ -298,15 +298,26 @@ Here are some guidelines: The user is looking to do a query on their data source named: ${source.name} of type ${source.kind}. -The ${source.kind === SourceKind.Log ? 'log level' : 'span status code'} is stored in ${source.severityTextExpression}. -You can identify services via ${source.serviceNameExpression} +${ + source.kind === SourceKind.Log + ? `The log level is stored in ${source.severityTextExpression}.` + : source.kind === SourceKind.Trace + ? `The span status code is stored in ${source.statusCodeExpression}.` + : '' +} + +${'serviceNameExpression' in source ? `You can identify services via ${source.serviceNameExpression}` : ''} + ${ source.kind === SourceKind.Trace ? `Duration of spans can be queried via ${source.durationExpression} which is expressed in 10^-${source.durationPrecision} seconds of precision. Span names under ${source.spanNameExpression} and span kinds under ${source.spanKindExpression}` - : `The log body can be queried via ${source.bodyExpression}` + : source.kind === SourceKind.Log + ? `The log body can be queried via ${source.bodyExpression}` + : '' } -Various log/span-specific attributes as a Map can be found under ${source.eventAttributesExpression} while resource attributes that follow the OpenTelemetry semantic convention can be found under ${source.resourceAttributesExpression} +${'eventAttributesExpression' in source ? `Various log/span-specific attributes as a Map can be found under ${source.eventAttributesExpression}.` : ''} +${'resourceAttributesExpression' in source ? `Resource attributes that follow the OpenTelemetry semantic convention can be found under ${source.resourceAttributesExpression}.` : ''} You must use the full field name ex. "column['key']" or "column.key" as it appears. The following is a list of properties and example values that exist in the source: @@ -386,7 +397,7 @@ ${JSON.stringify(allFieldsWithKeys.slice(0, 200).map(f => ({ field: f.key, type: connection: connectionId, where: '', groupBy: resObject.groupBy, - timestampValueExpression: source.timestampValueExpression, + timestampValueExpression: source.timestampValueExpression ?? '', dateRange, granularity: 'auto', }; diff --git a/packages/api/src/routers/api/sources.ts b/packages/api/src/routers/api/sources.ts index 840a1d97c..05d94fc51 100644 --- a/packages/api/src/routers/api/sources.ts +++ b/packages/api/src/routers/api/sources.ts @@ -1,6 +1,7 @@ import { + SourceKind, SourceSchema, - sourceSchemaWithout, + SourceSchemaNoId, } from '@hyperdx/common-utils/dist/types'; import express from 'express'; import { z } from 'zod'; @@ -23,14 +24,28 @@ router.get('/', async (req, res, next) => { const sources = await getSources(teamId.toString()); - return res.json(sources.map(s => s.toJSON({ getters: true }))); + const out = sources.map(s => { + // Typescript gets confused about calling toJSON on the union type, but + // breaking it out into a switch statement keeps TS happy + switch (s.kind) { + case SourceKind.Log: + return s.toJSON({ getters: true }); + case SourceKind.Trace: + return s.toJSON({ getters: true }); + case SourceKind.Metric: + return s.toJSON({ getters: true }); + case SourceKind.Session: + return s.toJSON({ getters: true }); + default: + throw new Error(`Source found with invalid kind ${(s as any).kind}`); + } + }); + return res.json(out); } catch (e) { next(e); } }); -const SourceSchemaNoId = sourceSchemaWithout({ id: true }); - router.post( '/', validateRequest({ @@ -40,11 +55,10 @@ router.post( try { const { teamId } = getNonNullUserWithTeam(req); - // TODO: HDX-1768 Eliminate type assertion const source = await createSource(teamId.toString(), { ...req.body, team: teamId, - } as any); + }); res.json(source); } catch (e) { @@ -65,11 +79,10 @@ router.put( try { const { teamId } = getNonNullUserWithTeam(req); - // TODO: HDX-1768 Eliminate type assertion const source = await updateSource(teamId.toString(), req.params.id, { ...req.body, team: teamId, - } as any); + }); if (!source) { res.status(404).send('Source not found'); diff --git a/packages/api/src/routers/external-api/__tests__/charts.test.ts b/packages/api/src/routers/external-api/__tests__/charts.test.ts index f996a1e8e..3b4684772 100644 --- a/packages/api/src/routers/external-api/__tests__/charts.test.ts +++ b/packages/api/src/routers/external-api/__tests__/charts.test.ts @@ -1,5 +1,8 @@ -import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node'; -import { SourceKind } from '@hyperdx/common-utils/dist/types'; +import { + SourceKind, + type TLogSource, + type TMetricSource, +} from '@hyperdx/common-utils/dist/types'; import { MetricsDataType } from '@hyperdx/common-utils/dist/types'; import { ObjectId } from 'mongodb'; import request from 'supertest'; @@ -16,7 +19,7 @@ import { getServer, } from '../../../fixtures'; import Connection from '../../../models/connection'; -import { ISource, Source } from '../../../models/source'; +import { LogSource, MetricSource } from '../../../models/source'; // Default time range for tests (1 hour) const DEFAULT_END_TIME = Date.now(); @@ -77,8 +80,8 @@ describe('External API v2 Charts', () => { let team: any; let user: any; let connection: any; - let logSource: ISource; - let metricSource: ISource; + let logSource: TLogSource; + let metricSource: TMetricSource; beforeAll(async () => { await server.start(); @@ -98,19 +101,19 @@ describe('External API v2 Charts', () => { password: config.CLICKHOUSE_PASSWORD, }); - logSource = await Source.create({ - kind: SourceKind.Log, + logSource = await LogSource.create({ team: team._id, + name: 'Logs', + kind: SourceKind.Log, from: { databaseName: DEFAULT_DATABASE, tableName: DEFAULT_LOGS_TABLE, }, timestampValueExpression: 'Timestamp', connection: connection._id, - name: 'Logs', }); - metricSource = await Source.create({ + metricSource = await MetricSource.create({ kind: SourceKind.Metric, team: team._id, from: { diff --git a/packages/api/src/tasks/__tests__/checkAlerts.test.ts b/packages/api/src/tasks/__tests__/checkAlerts.test.ts index 77a7d8fb3..3782f997e 100644 --- a/packages/api/src/tasks/__tests__/checkAlerts.test.ts +++ b/packages/api/src/tasks/__tests__/checkAlerts.test.ts @@ -1,5 +1,5 @@ import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node'; -import { AlertState } from '@hyperdx/common-utils/dist/types'; +import { AlertState, SourceKind } from '@hyperdx/common-utils/dist/types'; import mongoose from 'mongoose'; import ms from 'ms'; @@ -19,7 +19,7 @@ import AlertHistory from '@/models/alertHistory'; import Connection from '@/models/connection'; import Dashboard from '@/models/dashboard'; import { SavedSearch } from '@/models/savedSearch'; -import { Source } from '@/models/source'; +import { LogSource, MetricSource } from '@/models/source'; import Webhook from '@/models/webhook'; import * as checkAlert from '@/tasks/checkAlerts'; import { @@ -121,9 +121,9 @@ describe('checkAlerts', () => { interval: '1m', }, source: { - id: 'fake-source-id' as any, - kind: 'log' as any, - team: 'team-123' as any, + id: 'fake-source-id', + kind: SourceKind.Log, + team: 'team-123', from: { databaseName: 'default', tableName: 'otel_logs', @@ -131,7 +131,7 @@ describe('checkAlerts', () => { timestampValueExpression: 'Timestamp', connection: 'connection-123' as any, name: 'Logs', - }, + } as any, savedSearch: { _id: 'fake-saved-search-id' as any, team: 'team-123' as any, @@ -670,7 +670,7 @@ describe('checkAlerts', () => { username: config.CLICKHOUSE_USER, password: config.CLICKHOUSE_PASSWORD, }); - const source = await Source.create({ + const source = (await LogSource.create({ kind: 'log', team: team._id, from: { @@ -680,7 +680,7 @@ describe('checkAlerts', () => { timestampValueExpression: 'Timestamp', connection: connection.id, name: 'Logs', - }); + })) as any; const savedSearch = await new SavedSearch({ team: team._id, name: 'My Search', @@ -912,7 +912,7 @@ describe('checkAlerts', () => { username: config.CLICKHOUSE_USER, password: config.CLICKHOUSE_PASSWORD, }); - const source = await Source.create({ + const source = (await LogSource.create({ kind: 'log', team: team._id, from: { @@ -922,7 +922,7 @@ describe('checkAlerts', () => { timestampValueExpression: 'Timestamp', connection: connection.id, name: 'Logs', - }); + })) as any; const dashboard = await new Dashboard({ name: 'My Dashboard', team: team._id, @@ -1162,7 +1162,7 @@ describe('checkAlerts', () => { username: config.CLICKHOUSE_USER, password: config.CLICKHOUSE_PASSWORD, }); - const source = await Source.create({ + const source = (await LogSource.create({ kind: 'log', team: team._id, from: { @@ -1172,7 +1172,7 @@ describe('checkAlerts', () => { timestampValueExpression: 'Timestamp', connection: connection.id, name: 'Logs', - }); + })) as any; const dashboard = await new Dashboard({ name: 'My Dashboard', team: team._id, @@ -1387,7 +1387,7 @@ describe('checkAlerts', () => { username: config.CLICKHOUSE_USER, password: config.CLICKHOUSE_PASSWORD, }); - const source = await Source.create({ + const source = (await MetricSource.create({ kind: 'metric', team: team._id, from: { @@ -1402,7 +1402,7 @@ describe('checkAlerts', () => { timestampValueExpression: 'TimeUnix', connection: connection.id, name: 'Metrics', - }); + })) as any; const dashboard = await new Dashboard({ name: 'My Dashboard', team: team._id, diff --git a/packages/api/src/tasks/__tests__/singleInvocationAlert.test.ts b/packages/api/src/tasks/__tests__/singleInvocationAlert.test.ts index 4a6f17c77..794e58ea3 100644 --- a/packages/api/src/tasks/__tests__/singleInvocationAlert.test.ts +++ b/packages/api/src/tasks/__tests__/singleInvocationAlert.test.ts @@ -12,7 +12,7 @@ import AlertHistory from '@/models/alertHistory'; import Connection from '@/models/connection'; import Dashboard from '@/models/dashboard'; import { SavedSearch } from '@/models/savedSearch'; -import { Source } from '@/models/source'; +import { LogSource } from '@/models/source'; import Webhook from '@/models/webhook'; import { processAlert } from '@/tasks/checkAlerts'; import { AlertDetails, AlertTaskType, loadProvider } from '@/tasks/providers'; @@ -100,7 +100,7 @@ describe('Single Invocation Alert Test', () => { }); // Create source - const source = await Source.create({ + const source = await LogSource.create({ kind: 'log', team: team._id, from: { @@ -288,7 +288,7 @@ describe('Single Invocation Alert Test', () => { }); // Create source - const source = await Source.create({ + const source = (await LogSource.create({ kind: 'log', team: team._id, from: { @@ -298,7 +298,7 @@ describe('Single Invocation Alert Test', () => { timestampValueExpression: 'Timestamp', connection: connection.id, name: 'Test Logs', - }); + })) as any; // Create dashboard with multiple tiles - the alerting tile is NOT the first one const dashboard = await new Dashboard({ diff --git a/packages/api/src/tasks/checkAlerts.ts b/packages/api/src/tasks/checkAlerts.ts index 1644ee32d..0adca985d 100644 --- a/packages/api/src/tasks/checkAlerts.ts +++ b/packages/api/src/tasks/checkAlerts.ts @@ -8,6 +8,7 @@ import { getMetadata, Metadata } from '@hyperdx/common-utils/dist/metadata'; import { ChartConfigWithOptDateRange, DisplayType, + SourceKind, } from '@hyperdx/common-utils/dist/types'; import { setTraceAttributes } from '@hyperdx/node-opentelemetry'; import * as fns from 'date-fns'; @@ -194,7 +195,10 @@ export const processAlert = async ( where: savedSearch.where, whereLanguage: savedSearch.whereLanguage, groupBy: alert.groupBy, - implicitColumnExpression: source.implicitColumnExpression, + implicitColumnExpression: + source.kind === SourceKind.Trace || source.kind === SourceKind.Log + ? source.implicitColumnExpression + : undefined, timestampValueExpression: source.timestampValueExpression, }; } else if (details.taskType === AlertTaskType.TILE) { @@ -210,8 +214,12 @@ export const processAlert = async ( from: source.from, granularity: `${windowSizeInMins} minute`, groupBy: tile.config.groupBy, - implicitColumnExpression: source.implicitColumnExpression, - metricTables: source.metricTables, + implicitColumnExpression: + source.kind === SourceKind.Trace || source.kind === SourceKind.Log + ? source.implicitColumnExpression + : undefined, + metricTables: + source.kind === SourceKind.Metric ? source.metricTables : undefined, select: tile.config.select, timestampValueExpression: source.timestampValueExpression, where: tile.config.where, diff --git a/packages/api/src/tasks/providers/__tests__/default.test.ts b/packages/api/src/tasks/providers/__tests__/default.test.ts index 30b3f1dcd..63ec3653a 100644 --- a/packages/api/src/tasks/providers/__tests__/default.test.ts +++ b/packages/api/src/tasks/providers/__tests__/default.test.ts @@ -7,7 +7,7 @@ import Alert, { AlertSource, AlertThresholdType } from '@/models/alert'; import Connection from '@/models/connection'; import Dashboard from '@/models/dashboard'; import { SavedSearch } from '@/models/savedSearch'; -import { Source } from '@/models/source'; +import { LogSource } from '@/models/source'; import { AlertProvider, AlertTaskType, @@ -57,7 +57,7 @@ describe('DefaultAlertProvider', () => { }); // Create source - const source = await Source.create({ + const source = await LogSource.create({ team: team._id, name: 'Test Source', kind: 'log', @@ -125,7 +125,7 @@ describe('DefaultAlertProvider', () => { }); // Create source - const source = await Source.create({ + const source = await LogSource.create({ team: team._id, name: 'Test Source', kind: 'log', @@ -250,7 +250,7 @@ describe('DefaultAlertProvider', () => { }); // Create source - const source = await Source.create({ + const source = await LogSource.create({ team: team._id, name: 'Test Source', kind: 'log', @@ -348,7 +348,7 @@ describe('DefaultAlertProvider', () => { }); // Create sources for each connection - const source1 = await Source.create({ + const source1 = await LogSource.create({ team: team._id, name: 'Source 1', kind: 'log', @@ -360,7 +360,7 @@ describe('DefaultAlertProvider', () => { connection: connection1._id, }); - const source2 = await Source.create({ + const source2 = await LogSource.create({ team: team._id, name: 'Source 2', kind: 'log', @@ -525,7 +525,7 @@ describe('DefaultAlertProvider', () => { const team = await createTeam({ name: 'Test Team' }); // Create source with non-existent connection - const source = await Source.create({ + const source = await LogSource.create({ team: team._id, name: 'Test Source', kind: 'log', @@ -599,7 +599,7 @@ describe('DefaultAlertProvider', () => { }); // Create source - const source = await Source.create({ + const source = await LogSource.create({ team: team._id, name: 'Test Source', kind: 'log', @@ -661,7 +661,7 @@ describe('DefaultAlertProvider', () => { }); // Create source - const source = await Source.create({ + const source = await LogSource.create({ team: team._id, name: 'Test Source', kind: 'log', diff --git a/packages/api/src/tasks/template.ts b/packages/api/src/tasks/template.ts index 6d99a71c4..6f287c315 100644 --- a/packages/api/src/tasks/template.ts +++ b/packages/api/src/tasks/template.ts @@ -5,6 +5,7 @@ import { AlertChannelType, ChartConfigWithOptDateRange, DisplayType, + SourceKind, WebhookService, zAlertChannelType, } from '@hyperdx/common-utils/dist/types'; @@ -475,10 +476,17 @@ export const renderAlertTemplate = async ({ displayType: DisplayType.Search, dateRange: [startTime, endTime], from: source.from, - select: savedSearch.select || source.defaultTableSelectExpression || '', // remove alert body if there is no select and defaultTableSelectExpression + select: + savedSearch.select || + ('defaultTableSelectExpression' in source && + source.defaultTableSelectExpression) || + '', // remove alert body if there is no select and defaultTableSelectExpression where: savedSearch.where, whereLanguage: savedSearch.whereLanguage, - implicitColumnExpression: source.implicitColumnExpression, + implicitColumnExpression: + source.kind === SourceKind.Trace || source.kind === SourceKind.Log + ? source.implicitColumnExpression + : undefined, timestampValueExpression: source.timestampValueExpression, orderBy: savedSearch.orderBy, limit: { diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index 55b89a84f..dd3f57570 100644 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -3,20 +3,12 @@ "compilerOptions": { "baseUrl": "./src", "paths": { - "@/*": [ - "./*" - ] + "@/*": ["./*"] }, "rootDir": ".", "outDir": "build", "moduleResolution": "Node16" }, - "include": [ - "src", - "migrations", - "scripts" - ], - "exclude": [ - "node_modules" - ] -} \ No newline at end of file + "include": ["src", "migrations", "scripts"], + "exclude": ["node_modules"] +} diff --git a/packages/app/src/ChartUtils.tsx b/packages/app/src/ChartUtils.tsx index 1f6d64926..639be4cb7 100644 --- a/packages/app/src/ChartUtils.tsx +++ b/packages/app/src/ChartUtils.tsx @@ -16,7 +16,10 @@ import { SavedChartConfig, SourceKind, SQLInterval, - TSource, + type TLogSource, + type TMetricSource, + type TSource, + type TTraceSource, } from '@hyperdx/common-utils/dist/types'; import { SegmentedControl, Select as MSelect } from '@mantine/core'; @@ -520,7 +523,9 @@ export function formatResponseForTimeChart({ groupColumns[0].name === (source.kind === SourceKind.Log ? source.severityTextExpression - : source.statusCodeExpression) + : source.kind === SourceKind.Trace + ? source.statusCodeExpression + : undefined) ) { color = logLevelColor(row[groupColumns[0].name]); } @@ -648,7 +653,7 @@ export const mapV1AggFnToV2 = (aggFn?: AggFn): AggFnV2 | undefined => { }; export const convertV1GroupByToV2 = ( - metricSource: TSource, + metricSource: TMetricSource, groupBy: string[], ): string => { return groupBy @@ -674,9 +679,9 @@ export const convertV1ChartConfigToV2 = ( sortOrder?: SortOrder; }, source: { - log?: TSource; - metric?: TSource; - trace?: TSource; + log?: TLogSource; + metric?: TMetricSource; + trace?: TTraceSource; }, ): ChartConfigWithDateRange => { const { diff --git a/packages/app/src/DBDashboardPage.tsx b/packages/app/src/DBDashboardPage.tsx index 1680571ce..527f371d7 100644 --- a/packages/app/src/DBDashboardPage.tsx +++ b/packages/app/src/DBDashboardPage.tsx @@ -18,7 +18,7 @@ import { TableConnection } from '@hyperdx/common-utils/dist/metadata'; import { AlertState, DashboardFilter, - TSourceUnion, + TSource, } from '@hyperdx/common-utils/dist/types'; import { ChartConfigWithDateRange, @@ -178,14 +178,18 @@ const Tile = forwardRef( connection: source.connection, dateRange, granularity, - timestampValueExpression: source.timestampValueExpression, + timestampValueExpression: source.timestampValueExpression ?? '', from: { databaseName: source.from?.databaseName || 'default', tableName: tableName || '', }, - implicitColumnExpression: source.implicitColumnExpression, + ...('implicitColumnExpression' in source + ? { implicitColumnExpression: source.implicitColumnExpression } + : {}), filters, - metricTables: source.metricTables, + ...('metricTables' in source + ? { metricTables: source.metricTables } + : {}), }); } } @@ -352,7 +356,9 @@ const Tile = forwardRef( dateRange, select: queriedConfig.select || - source?.defaultTableSelectExpression || + (source && 'defaultTableSelectExpression' in source + ? source.defaultTableSelectExpression + : undefined) || '', groupBy: undefined, granularity: undefined, @@ -953,7 +959,7 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) { convertToDashboardTemplate( dashboard, // TODO: fix this type issue - sources as TSourceUnion[], + sources, ), dashboard?.name, ); diff --git a/packages/app/src/DBSearchPage.tsx b/packages/app/src/DBSearchPage.tsx index e58a4a100..016310a3a 100644 --- a/packages/app/src/DBSearchPage.tsx +++ b/packages/app/src/DBSearchPage.tsx @@ -137,6 +137,8 @@ const SearchConfigSchema = z.object({ type SearchConfigFromSchema = z.infer; +const ALLOWED_SOURCE_KINDS = [SourceKind.Log, SourceKind.Trace]; + // Helper function to get the default source id export function getDefaultSourceId( sources: { id: string }[] | undefined, @@ -485,9 +487,16 @@ function useSearchedConfigToChartConfig({ filters, orderBy, }: SearchConfig) { - const { data: sourceObj, isLoading } = useSource({ + const { data: logSource, isLoading: isLogSourceLoading } = useSource({ + id: source, + kind: SourceKind.Log, + }); + const { data: traceSource, isLoading: isTraceSourceLoading } = useSource({ id: source, + kind: SourceKind.Trace, }); + const sourceObj = logSource ?? traceSource; + const isLoading = isLogSourceLoading || isTraceSourceLoading; const defaultOrderBy = useDefaultOrderBy(source); return useMemo(() => { @@ -496,7 +505,8 @@ function useSearchedConfigToChartConfig({ data: { select: select || (sourceObj.defaultTableSelectExpression ?? ''), from: sourceObj.from, - ...(sourceObj.tableFilterExpression != null + ...('tableFilterExpression' in sourceObj && + sourceObj.tableFilterExpression != null ? { filters: [ { @@ -619,9 +629,15 @@ function DBSearchPage() { 'hdx-last-selected-source-id', '', ); - const { data: searchedSource } = useSource({ + const { data: logSearchedSource } = useSource({ + id: searchedConfig.source, + kind: SourceKind.Log, + }); + const { data: traceSearchedSource } = useSource({ id: searchedConfig.source, + kind: SourceKind.Trace, }); + const searchedSource = logSearchedSource ?? traceSearchedSource; const [analysisMode, setAnalysisMode] = useQueryState( 'mode', @@ -693,7 +709,15 @@ function DBSearchPage() { const inputSource = watch('source'); // const { data: inputSourceObj } = useSource({ id: inputSource }); const { data: inputSourceObjs } = useSources(); - const inputSourceObj = inputSourceObjs?.find(s => s.id === inputSource); + const { data: logSource } = useSource({ + id: inputSource, + kind: SourceKind.Log, + }); + const { data: traceSource } = useSource({ + id: inputSource, + kind: SourceKind.Trace, + }); + const inputSourceObj = logSource ?? traceSource; const defaultOrderBy = useDefaultOrderBy(inputSource); @@ -850,10 +874,12 @@ function DBSearchPage() { // Save the selected source ID to localStorage setLastSelectedSourceId(newInputSourceObj.id); - setValue( - 'select', - newInputSourceObj?.defaultTableSelectExpression ?? '', - ); + if (newInputSourceObj.kind === SourceKind.Log) { + setValue( + 'select', + newInputSourceObj.defaultTableSelectExpression ?? '', + ); + } // Clear all search filters searchFilters.clearAllFilters(); } @@ -1236,10 +1262,13 @@ function DBSearchPage() { control={control} name="source" onCreate={openNewSourceModal} - allowedSourceKinds={[SourceKind.Log, SourceKind.Trace]} + allowedSourceKinds={ALLOWED_SOURCE_KINDS} data-testid="source-selector" sourceSchemaPreview={ - + } /> @@ -1528,7 +1557,10 @@ function DBSearchPage() { setAnalysisMode={setAnalysisMode} chartConfig={filtersChartConfig} sourceId={inputSourceObj?.id} - showDelta={!!searchedSource?.durationExpression} + showDelta={ + searchedSource?.kind === SourceKind.Trace && + !!searchedSource?.durationExpression + } {...searchFilters} /> @@ -1579,8 +1611,9 @@ function DBSearchPage() { dateRange: searchedTimeRange, }} bodyValueExpression={ - searchedSource?.bodyExpression ?? - chartConfig.implicitColumnExpression ?? + ((searchedSource?.kind === SourceKind.Log && + searchedSource?.bodyExpression) || + chartConfig.implicitColumnExpression) ?? '' } totalCountConfig={histogramTimeChartConfig} @@ -1588,59 +1621,61 @@ function DBSearchPage() { /> )} - {analysisMode === 'delta' && searchedSource != null && ( - -
- { - setOutlierSqlCondition( - [ - `${searchedSource.durationExpression} >= ${yMin} * 1e${(searchedSource.durationPrecision ?? 9) - 3}`, - `${searchedSource.durationExpression} <= ${yMax} * 1e${(searchedSource.durationPrecision ?? 9) - 3}`, - `${getFirstTimestampValueExpression(chartConfig.timestampValueExpression)} >= ${xMin}`, - `${getFirstTimestampValueExpression(chartConfig.timestampValueExpression)} <= ${xMax}`, - ].join(' AND '), - ); - }} - /> -
- {outlierSqlCondition ? ( - - ) : ( - -
- - Please highlight an outlier range in the heatmap to - view the delta chart. - -
-
- )} -
- )} + {analysisMode === 'delta' && + searchedSource != null && + searchedSource.kind === SourceKind.Trace && ( + +
+ { + setOutlierSqlCondition( + [ + `${searchedSource.durationExpression} >= ${yMin} * 1e${(searchedSource.durationPrecision ?? 9) - 3}`, + `${searchedSource.durationExpression} <= ${yMax} * 1e${(searchedSource.durationPrecision ?? 9) - 3}`, + `${getFirstTimestampValueExpression(chartConfig.timestampValueExpression)} >= ${xMin}`, + `${getFirstTimestampValueExpression(chartConfig.timestampValueExpression)} <= ${xMax}`, + ].join(' AND '), + ); + }} + /> +
+ {outlierSqlCondition ? ( + + ) : ( + +
+ + Please highlight an outlier range in the heatmap to + view the delta chart. + +
+
+ )} +
+ )}
{analysisMode === 'results' && chartConfig && diff --git a/packages/app/src/DashboardFiltersModal.tsx b/packages/app/src/DashboardFiltersModal.tsx index df798a331..46a3432b9 100644 --- a/packages/app/src/DashboardFiltersModal.tsx +++ b/packages/app/src/DashboardFiltersModal.tsx @@ -1,6 +1,9 @@ import { useEffect, useState } from 'react'; import { Controller, FieldError, useForm } from 'react-hook-form'; -import { TableConnection } from '@hyperdx/common-utils/dist/metadata'; +import { + TableConnection, + tcFromSource, +} from '@hyperdx/common-utils/dist/metadata'; import { DashboardFilter, MetricsDataType, @@ -107,7 +110,7 @@ const DashboardFilterEditForm = ({ const sourceIsMetric = source?.kind === SourceKind.Metric; const metricTypes = Object.values(MetricsDataType).filter( - type => source?.metricTables?.[type], + type => source?.kind === SourceKind.Metric && source?.metricTables?.[type], ); const [modalContentRef, setModalContentRef] = useState( @@ -142,7 +145,10 @@ const DashboardFilterEditForm = ({ rules={{ required: true }} comboboxProps={{ withinPortal: true }} sourceSchemaPreview={ - + } /> diff --git a/packages/app/src/KubernetesDashboardPage.tsx b/packages/app/src/KubernetesDashboardPage.tsx index 5856e1431..bd5fde330 100644 --- a/packages/app/src/KubernetesDashboardPage.tsx +++ b/packages/app/src/KubernetesDashboardPage.tsx @@ -7,8 +7,14 @@ import sub from 'date-fns/sub'; import { useQueryState } from 'nuqs'; import { useForm } from 'react-hook-form'; import { StringParam, useQueryParam, withDefault } from 'use-query-params'; -import { SourceKind, TSource } from '@hyperdx/common-utils/dist/types'; +import { tcFromSource } from '@hyperdx/common-utils/dist/metadata'; import { + SourceKind, + type TMetricSource, + type TSource, +} from '@hyperdx/common-utils/dist/types'; +import { + Anchor, Badge, Box, Card, @@ -44,7 +50,7 @@ import { withAppNav } from './layout'; import NamespaceDetailsSidePanel from './NamespaceDetailsSidePanel'; import NodeDetailsSidePanel from './NodeDetailsSidePanel'; import PodDetailsSidePanel from './PodDetailsSidePanel'; -import { useSources } from './source'; +import { useSource, useSources } from './source'; import { parseTimeQuery, useTimeQuery } from './timeQuery'; import { KubePhase } from './types'; import { formatNumber, formatUptime } from './utils'; @@ -134,7 +140,7 @@ export const InfraPodsStatusTable = ({ where, }: { dateRange: [Date, Date]; - metricSource: TSource; + metricSource: TMetricSource; where: string; }) => { const [phaseFilter, setPhaseFilter] = React.useState('running'); @@ -431,7 +437,7 @@ const NodesTable = ({ where, dateRange, }: { - metricSource: TSource; + metricSource: TMetricSource; where: string; dateRange: [Date, Date]; }) => { @@ -600,7 +606,7 @@ const NamespacesTable = ({ where, }: { dateRange: [Date, Date]; - metricSource: TSource; + metricSource: TMetricSource; where: string; }) => { const groupBy = ['k8s.namespace.name']; @@ -820,9 +826,15 @@ function KubernetesDashboardPage() { () => resolveSourceIds(_logSourceId, _metricSourceId, sources), [_logSourceId, _metricSourceId, sources], ); - - const logSource = sources?.find(s => s.id === logSourceId); - const metricSource = sources?.find(s => s.id === metricSourceId); + // TODO: Let users select log + metric sources + const { data: logSource } = useSource({ + id: logSourceId, + kind: SourceKind.Log, + }); + const { data: metricSource } = useSource({ + id: metricSourceId, + kind: SourceKind.Metric, + }); const { control, watch } = useForm({ values: { @@ -910,7 +922,10 @@ function KubernetesDashboardPage() { size="xs" allowDeselect={false} sourceSchemaPreview={ - + } /> + } /> diff --git a/packages/app/src/NamespaceDetailsSidePanel.tsx b/packages/app/src/NamespaceDetailsSidePanel.tsx index 7a5051212..26f65a178 100644 --- a/packages/app/src/NamespaceDetailsSidePanel.tsx +++ b/packages/app/src/NamespaceDetailsSidePanel.tsx @@ -3,7 +3,10 @@ import Link from 'next/link'; import Drawer from 'react-modern-drawer'; import { StringParam, useQueryParam, withDefault } from 'use-query-params'; import { tcFromSource } from '@hyperdx/common-utils/dist/metadata'; -import { TSource } from '@hyperdx/common-utils/dist/types'; +import type { + TLogSource, + TMetricSource, +} from '@hyperdx/common-utils/dist/types'; import { Anchor, Badge, @@ -61,7 +64,7 @@ const NamespaceDetails = ({ }: { name: string; dateRange: [Date, Date]; - metricSource?: TSource; + metricSource?: TMetricSource; }) => { const where = `${metricSource?.resourceAttributesExpression}.k8s.namespace.name:"${name}"`; const groupBy = ['k8s.namespace.name']; @@ -144,7 +147,7 @@ function NamespaceLogs({ where, }: { dateRange: [Date, Date]; - logSource: TSource; + logSource: TLogSource; where: string; }) { const [resultType, setResultType] = React.useState<'all' | 'error'>('all'); @@ -232,8 +235,8 @@ export default function NamespaceDetailsSidePanel({ metricSource, logSource, }: { - metricSource: TSource; - logSource: TSource; + metricSource: TMetricSource; + logSource: TLogSource; }) { const [namespaceName, setNamespaceName] = useQueryParam( 'namespaceName', diff --git a/packages/app/src/NodeDetailsSidePanel.tsx b/packages/app/src/NodeDetailsSidePanel.tsx index 1deca91ad..198e530d8 100644 --- a/packages/app/src/NodeDetailsSidePanel.tsx +++ b/packages/app/src/NodeDetailsSidePanel.tsx @@ -3,9 +3,9 @@ import Link from 'next/link'; import Drawer from 'react-modern-drawer'; import { StringParam, useQueryParam, withDefault } from 'use-query-params'; import { tcFromSource } from '@hyperdx/common-utils/dist/metadata'; -import { - SearchConditionLanguage, - TSource, +import type { + TLogSource, + TMetricSource, } from '@hyperdx/common-utils/dist/types'; import { Anchor, @@ -65,7 +65,7 @@ const NodeDetails = ({ }: { name: string; dateRange: [Date, Date]; - metricSource: TSource; + metricSource: TMetricSource; }) => { const where = `${metricSource.resourceAttributesExpression}.k8s.node.name:"${name}"`; const groupBy = ['k8s.node.name']; @@ -160,7 +160,7 @@ function NodeLogs({ where, }: { dateRange: [Date, Date]; - logSource: TSource; + logSource: TLogSource; where: string; }) { const [resultType, setResultType] = React.useState<'all' | 'error'>('all'); @@ -248,8 +248,8 @@ export default function NodeDetailsSidePanel({ metricSource, logSource, }: { - metricSource: TSource; - logSource: TSource; + metricSource: TMetricSource; + logSource: TLogSource; }) { const [nodeName, setNodeName] = useQueryParam( 'nodeName', diff --git a/packages/app/src/PodDetailsSidePanel.tsx b/packages/app/src/PodDetailsSidePanel.tsx index a1b274e0c..64ecaa057 100644 --- a/packages/app/src/PodDetailsSidePanel.tsx +++ b/packages/app/src/PodDetailsSidePanel.tsx @@ -3,7 +3,10 @@ import Link from 'next/link'; import Drawer from 'react-modern-drawer'; import { StringParam, useQueryParam, withDefault } from 'use-query-params'; import { tcFromSource } from '@hyperdx/common-utils/dist/metadata'; -import { TSource } from '@hyperdx/common-utils/dist/types'; +import type { + TLogSource, + TMetricSource, +} from '@hyperdx/common-utils/dist/types'; import { Anchor, Box, @@ -59,7 +62,7 @@ const PodDetails = ({ podName, }: { dateRange: [Date, Date]; - logSource: TSource; + logSource: TLogSource; podName: string; }) => { const { data: logsData } = useV2LogBatch<{ @@ -137,7 +140,7 @@ function PodLogs({ onRowClick, }: { dateRange: [Date, Date]; - logSource: TSource; + logSource: TLogSource; where: string; rowId: string | null; onRowClick: (rowId: string) => void; @@ -223,8 +226,8 @@ export default function PodDetailsSidePanel({ logSource, metricSource, }: { - logSource: TSource; - metricSource: TSource; + logSource: TLogSource; + metricSource: TMetricSource; }) { const [podName, setPodName] = useQueryParam( 'podName', diff --git a/packages/app/src/ServicesDashboardPage.tsx b/packages/app/src/ServicesDashboardPage.tsx index 9e336e35d..bb79cdba5 100644 --- a/packages/app/src/ServicesDashboardPage.tsx +++ b/packages/app/src/ServicesDashboardPage.tsx @@ -12,7 +12,7 @@ import { DisplayType, Filter, SourceKind, - TSource, + type TTraceSource, } from '@hyperdx/common-utils/dist/types'; import { Box, @@ -59,7 +59,7 @@ type AppliedConfig = { }; function getScopedFilters( - source: TSource, + source: TTraceSource, appliedConfig: AppliedConfig, includeIsSpanKindServer = true, ): Filter[] { @@ -90,12 +90,8 @@ function ServiceSelectControlled({ size?: string; onCreate?: () => void; } & UseControllerProps) { - const { data: source } = useSource({ id: sourceId }); - const { data: jsonColumns = [] } = useJsonColumns({ - databaseName: source?.from?.databaseName || '', - tableName: source?.from?.tableName || '', - connectionId: source?.connection || '', - }); + const { data: source } = useSource({ id: sourceId, kind: SourceKind.Trace }); + const { data: jsonColumns = [] } = useJsonColumns(tcFromSource(source)); const expressions = getExpressions(source, jsonColumns); const queriedConfig = { @@ -154,16 +150,12 @@ export function EndpointLatencyChart({ appliedConfig = {}, extraFilters = [], }: { - source: TSource; + source: TTraceSource; dateRange: [Date, Date]; appliedConfig?: AppliedConfig; extraFilters?: Filter[]; }) { - const { data: jsonColumns = [] } = useJsonColumns({ - databaseName: source?.from?.databaseName || '', - tableName: source?.from?.tableName || '', - connectionId: source?.connection || '', - }); + const { data: jsonColumns = [] } = useJsonColumns(tcFromSource(source)); const expressions = getExpressions(source, jsonColumns); const [latencyChartType, setLatencyChartType] = useState< 'line' | 'histogram' @@ -269,12 +261,11 @@ function HttpTab({ searchedTimeRange: [Date, Date]; appliedConfig: AppliedConfig; }) { - const { data: source } = useSource({ id: appliedConfig.source }); - const { data: jsonColumns = [] } = useJsonColumns({ - databaseName: source?.from?.databaseName || '', - tableName: source?.from?.tableName || '', - connectionId: source?.connection || '', + const { data: source } = useSource({ + id: appliedConfig.source, + kind: ALLOWED_SOURCE_KIND, }); + const { data: jsonColumns = [] } = useJsonColumns(tcFromSource(source)); const expressions = getExpressions(source, jsonColumns); const [reqChartType, setReqChartType] = useQueryState( @@ -546,12 +537,11 @@ function DatabaseTab({ searchedTimeRange: [Date, Date]; appliedConfig: AppliedConfig; }) { - const { data: source } = useSource({ id: appliedConfig.source }); - const { data: jsonColumns = [] } = useJsonColumns({ - databaseName: source?.from?.databaseName || '', - tableName: source?.from?.tableName || '', - connectionId: source?.connection || '', + const { data: source } = useSource({ + id: appliedConfig.source, + kind: ALLOWED_SOURCE_KIND, }); + const { data: jsonColumns = [] } = useJsonColumns(tcFromSource(source)); const expressions = getExpressions(source, jsonColumns); const [chartType, setChartType] = useState<'table' | 'list'>('list'); @@ -800,7 +790,10 @@ function ErrorsTab({ searchedTimeRange: [Date, Date]; appliedConfig: AppliedConfig; }) { - const { data: source } = useSource({ id: appliedConfig.source }); + const { data: source } = useSource({ + id: appliedConfig.source, + kind: SourceKind.Trace, + }); const { data: jsonColumns = [] } = useJsonColumns({ databaseName: source?.from?.databaseName || '', tableName: source?.from?.tableName || '', @@ -849,6 +842,7 @@ function ErrorsTab({ ); } +const ALLOWED_SOURCE_KIND = SourceKind.Trace; // TODO: This is a hack to set the default time range const defaultTimeRange = parseTimeQuery('Past 1h', false) as [Date, Date]; @@ -883,6 +877,7 @@ function ServicesDashboardPage() { const sourceId = watch('source'); const { data: source } = useSource({ id: watch('source'), + kind: ALLOWED_SOURCE_KIND, }); useEffect(() => { @@ -943,7 +938,7 @@ function ServicesDashboardPage() { string; generateChartUrl?: (config: { diff --git a/packages/app/src/SessionsPage.tsx b/packages/app/src/SessionsPage.tsx index 0ab4fb317..2c3c2cfa0 100644 --- a/packages/app/src/SessionsPage.tsx +++ b/packages/app/src/SessionsPage.tsx @@ -254,10 +254,12 @@ export default function SessionsPage() { const sourceId = watch('source'); const { data: sessionSource, isPending: isSessionSourceLoading } = useSource({ id: watch('source'), + kind: SourceKind.Session, }); const { data: traceTrace } = useSource({ id: sessionSource?.traceSourceId, + kind: SourceKind.Trace, }); // Get all sources and select the first session type source by default diff --git a/packages/app/src/__tests__/serviceDashboard.test.ts b/packages/app/src/__tests__/serviceDashboard.test.ts index 827d944e0..a31384a19 100644 --- a/packages/app/src/__tests__/serviceDashboard.test.ts +++ b/packages/app/src/__tests__/serviceDashboard.test.ts @@ -1,4 +1,4 @@ -import type { TSource } from '@hyperdx/common-utils/dist/types'; +import type { TTraceSource } from '@hyperdx/common-utils/dist/types'; import { SourceKind } from '@hyperdx/common-utils/dist/types'; import { @@ -11,7 +11,7 @@ function removeAllWhitespace(str: string) { } describe('Service Dashboard', () => { - const mockSource: TSource = { + const mockSource: TTraceSource = { id: 'test-source', name: 'Test Source', kind: SourceKind.Trace, @@ -27,7 +27,9 @@ describe('Service Dashboard', () => { serviceNameExpression: 'ServiceName', spanNameExpression: 'SpanName', spanKindExpression: 'SpanKind', - severityTextExpression: 'StatusCode', + statusCodeExpression: 'StatusCode', + spanIdExpression: 'SpanId', + parentSpanIdExpression: 'ParentSpanId', }; describe('getExpressions', () => { diff --git a/packages/app/src/__tests__/utils.test.ts b/packages/app/src/__tests__/utils.test.ts index 059415a8f..7a2399366 100644 --- a/packages/app/src/__tests__/utils.test.ts +++ b/packages/app/src/__tests__/utils.test.ts @@ -1,4 +1,4 @@ -import { TSource } from '@hyperdx/common-utils/dist/types'; +import { TMetricSource } from '@hyperdx/common-utils/dist/types'; import { act, renderHook } from '@testing-library/react'; import { MetricsDataType, NumberFormat } from '../types'; @@ -65,14 +65,14 @@ describe('getMetricTableName', () => { }); it('returns the default table name when metricType is null', () => { - const source = createSourceWithMetrics() as unknown as TSource; + const source = createSourceWithMetrics() as unknown as TMetricSource; expect(getMetricTableName(source)).toBe(''); expect(getMetricTableName(source, undefined)).toBe(''); }); it('returns the specific metric table when metricType is provided', () => { - const source = createSourceWithMetrics() as unknown as TSource; + const source = createSourceWithMetrics() as unknown as TMetricSource; expect(getMetricTableName(source, 'gauge' as MetricsDataType)).toBe( 'gauge_table', @@ -83,7 +83,7 @@ describe('getMetricTableName', () => { }); it('handles case insensitivity for metric types', () => { - const source = createSourceWithMetrics() as unknown as TSource; + const source = createSourceWithMetrics() as unknown as TMetricSource; expect(getMetricTableName(source, 'GAUGE' as MetricsDataType)).toBe( 'gauge_table', @@ -99,7 +99,7 @@ describe('getMetricTableName', () => { metricTables: { gauge: 'gauge_table', }, - } as unknown as TSource; + } as unknown as TMetricSource; expect( getMetricTableName(source, 'histogram' as MetricsDataType), @@ -107,7 +107,7 @@ describe('getMetricTableName', () => { }); it('handles sources without metricTables property', () => { - const source = createBaseSource() as unknown as TSource; + const source = createBaseSource() as unknown as TMetricSource; expect(getMetricTableName(source)).toBe(''); expect( diff --git a/packages/app/src/components/AlertPreviewChart.tsx b/packages/app/src/components/AlertPreviewChart.tsx index 36ae7085d..592a1bd31 100644 --- a/packages/app/src/components/AlertPreviewChart.tsx +++ b/packages/app/src/components/AlertPreviewChart.tsx @@ -3,6 +3,7 @@ import { AlertInterval, SearchCondition, SearchConditionLanguage, + SourceKind, } from '@hyperdx/common-utils/dist/types'; import { TSource } from '@hyperdx/common-utils/dist/types'; import { Paper } from '@mantine/core'; @@ -62,7 +63,10 @@ export const AlertPreviewChart = ({ whereLanguage: whereLanguage || undefined, dateRange: intervalToDateRange(interval), granularity: intervalToGranularity(interval), - implicitColumnExpression: source.implicitColumnExpression, + implicitColumnExpression: + source.kind === SourceKind.Trace || source.kind === SourceKind.Log + ? source.implicitColumnExpression + : undefined, groupBy, with: aliasWith, select: [ @@ -73,7 +77,10 @@ export const AlertPreviewChart = ({ valueExpression: '', }, ], - timestampValueExpression: source.timestampValueExpression, + timestampValueExpression: + source.kind !== SourceKind.Session + ? source.timestampValueExpression + : '', from: source.from, connection: source.connection, }} diff --git a/packages/app/src/components/ContextSidePanel.tsx b/packages/app/src/components/ContextSidePanel.tsx index b6e7439cf..0dc01798b 100644 --- a/packages/app/src/components/ContextSidePanel.tsx +++ b/packages/app/src/components/ContextSidePanel.tsx @@ -6,7 +6,9 @@ import { useForm } from 'react-hook-form'; import { tcFromSource } from '@hyperdx/common-utils/dist/metadata'; import { ChartConfigWithDateRange, - TSource, + SourceKind, + type TLogSource, + type TTraceSource, } from '@hyperdx/common-utils/dist/types'; import { Badge, Flex, Group, SegmentedControl } from '@mantine/core'; import { useDebouncedValue } from '@mantine/hooks'; @@ -34,7 +36,7 @@ enum ContextBy { } interface ContextSubpanelProps { - source: TSource; + source: TLogSource | TTraceSource; dbSqlRowTableConfig: ChartConfigWithDateRange | undefined; rowData: Record; rowId: string | undefined; @@ -101,9 +103,15 @@ export default function ContextSubpanel({ setContextRowSource, } = useNestedPanelState(isNested); - const { data: contextRowSidePanelSource } = useSource({ + const { data: logSource } = useSource({ id: contextRowSource || '', + kind: SourceKind.Log, }); + const { data: traceSource } = useSource({ + id: contextRowSource || '', + kind: SourceKind.Trace, + }); + const contextRowSidePanelSource = logSource || traceSource; const handleContextSidePanelClose = useCallback(() => { setContextRowId(null); diff --git a/packages/app/src/components/DBEditTimeChartForm.tsx b/packages/app/src/components/DBEditTimeChartForm.tsx index 9a6b717aa..219e1a1b7 100644 --- a/packages/app/src/components/DBEditTimeChartForm.tsx +++ b/packages/app/src/components/DBEditTimeChartForm.tsx @@ -498,13 +498,20 @@ export default function EditTimeChartForm({ const newConfig = { ...form, from: tableSource.from, - timestampValueExpression: tableSource.timestampValueExpression, + timestampValueExpression: tableSource.timestampValueExpression || '', dateRange, connection: tableSource.connection, - implicitColumnExpression: tableSource.implicitColumnExpression, - metricTables: tableSource.metricTables, + ...(tableSource.kind === SourceKind.Log || + tableSource.kind === SourceKind.Trace + ? { implicitColumnExpression: tableSource.implicitColumnExpression } + : {}), + ...(tableSource.kind === SourceKind.Metric + ? { metricTables: tableSource.metricTables } + : {}), select: isSelectEmpty - ? tableSource.defaultTableSelectExpression || '' + ? ('defaultTableSelectExpression' in tableSource && + tableSource.defaultTableSelectExpression) || + '' : form.select, }; setQueriedConfig( @@ -578,27 +585,31 @@ export default function EditTimeChartForm({ const sampleEventsConfig = useMemo( () => tableSource != null && queriedConfig != null && queryReady - ? { + ? ({ ...queriedConfig, orderBy: [ { ordering: 'DESC' as const, valueExpression: getFirstTimestampValueExpression( - tableSource.timestampValueExpression, + tableSource.timestampValueExpression || '', ), }, ], dateRange, - timestampValueExpression: tableSource.timestampValueExpression, + timestampValueExpression: + tableSource.timestampValueExpression ?? '', connection: tableSource.connection, from: tableSource.from, limit: { limit: 200 }, - select: tableSource?.defaultTableSelectExpression || '', + select: + ('defaultTableSelectExpression' in tableSource + ? tableSource.defaultTableSelectExpression + : undefined) || '', filters: seriesToFilters(queriedConfig.select), filtersLogicalOperator: 'OR' as const, groupBy: undefined, granularity: undefined, - } + } satisfies ChartConfigWithDateRange) : null, [queriedConfig, tableSource, dateRange, queryReady], ); @@ -696,7 +707,15 @@ export default function EditTimeChartForm({ name="source" data-testid="source-selector" sourceSchemaPreview={ - + } /> @@ -821,9 +840,16 @@ export default function EditTimeChartForm({ control={control} name="select" placeholder={ - tableSource?.defaultTableSelectExpression || 'SELECT Columns' + (tableSource && + 'defaultTableSelectExpression' in tableSource && + tableSource.defaultTableSelectExpression) || + 'SELECT Columns' + } + defaultValue={ + tableSource && 'defaultTableSelectExpression' in tableSource + ? tableSource.defaultTableSelectExpression + : undefined } - defaultValue={tableSource?.defaultTableSelectExpression} onSubmit={onSubmit} label="SELECT" /> @@ -1046,18 +1072,20 @@ export default function EditTimeChartForm({ { ordering: 'DESC' as const, valueExpression: getFirstTimestampValueExpression( - tableSource.timestampValueExpression, + tableSource.timestampValueExpression ?? '', ), }, ], dateRange, - timestampValueExpression: tableSource.timestampValueExpression, + timestampValueExpression: + tableSource.timestampValueExpression ?? '', connection: tableSource.connection, from: tableSource.from, limit: { limit: 200 }, select: queriedConfig.select || - tableSource?.defaultTableSelectExpression || + ('defaultTableSelectExpression' in tableSource && + tableSource.defaultTableSelectExpression) || '', groupBy: undefined, granularity: undefined, diff --git a/packages/app/src/components/DBInfraPanel.tsx b/packages/app/src/components/DBInfraPanel.tsx index 1b79c9ebd..25b7a31f4 100644 --- a/packages/app/src/components/DBInfraPanel.tsx +++ b/packages/app/src/components/DBInfraPanel.tsx @@ -1,6 +1,10 @@ import { useMemo, useState } from 'react'; import { add, min, sub } from 'date-fns'; -import { TSource } from '@hyperdx/common-utils/dist/types'; +import { + SourceKind, + type TLogSource, + type TMetricSource, +} from '@hyperdx/common-utils/dist/types'; import { Box, Card, @@ -33,7 +37,7 @@ const InfraSubpanelGroup = ({ where, }: { fieldPrefix: string; - metricSource: TSource; + metricSource: TMetricSource; timestamp: any; title: string; where: string; @@ -210,9 +214,12 @@ export default ({ }: { rowData?: Record; rowId: string | undefined | null; - source: TSource; + source: TLogSource; }) => { - const { data: metricSource } = useSource({ id: source.metricSourceId }); + const { data: metricSource } = useSource({ + id: source.metricSourceId, + kind: SourceKind.Metric, + }); const podUid = rowData?.__hdx_resource_attributes['k8s.pod.uid']; const nodeName = rowData?.__hdx_resource_attributes['k8s.node.name']; diff --git a/packages/app/src/components/DBRowDataPanel.tsx b/packages/app/src/components/DBRowDataPanel.tsx index c974d56cd..580238199 100644 --- a/packages/app/src/components/DBRowDataPanel.tsx +++ b/packages/app/src/components/DBRowDataPanel.tsx @@ -1,6 +1,6 @@ import { useMemo } from 'react'; import type { ResponseJSON } from '@hyperdx/common-utils/dist/clickhouse'; -import { SourceKind, TSource } from '@hyperdx/common-utils/dist/types'; +import { SourceKind, type TSource } from '@hyperdx/common-utils/dist/types'; import { Box } from '@mantine/core'; import { useQueriedChartConfig } from '@/hooks/useChartConfig'; @@ -17,11 +17,15 @@ export function useRowData({ }) { const eventBodyExpr = getEventBody(source); - const searchedTraceIdExpr = source.traceIdExpression; - const searchedSpanIdExpr = source.spanIdExpression; + const searchedTraceIdExpr = + 'traceIdExpression' in source ? source.traceIdExpression : undefined; + const searchedSpanIdExpr = + 'spanIdExpression' in source ? source.spanIdExpression : undefined; const severityTextExpr = - source.severityTextExpression || source.statusCodeExpression; + (source.kind === SourceKind.Log && source.severityTextExpression) || + (source.kind === SourceKind.Trace && source.statusCodeExpression) || + undefined; return useQueriedChartConfig( { @@ -66,26 +70,26 @@ export function useRowData({ }, ] : []), - ...(source.serviceNameExpression + ...('serviceNameExpression' in source ? [ { - valueExpression: source.serviceNameExpression, + valueExpression: source.serviceNameExpression ?? '', alias: '__hdx_service_name', }, ] : []), - ...(source.resourceAttributesExpression + ...('resourceAttributesExpression' in source ? [ { - valueExpression: source.resourceAttributesExpression, + valueExpression: source.resourceAttributesExpression ?? '', alias: '__hdx_resource_attributes', }, ] : []), - ...(source.eventAttributesExpression + ...('eventAttributesExpression' in source ? [ { - valueExpression: source.eventAttributesExpression, + valueExpression: source.eventAttributesExpression ?? '', alias: '__hdx_event_attributes', }, ] diff --git a/packages/app/src/components/DBRowOverviewPanel.tsx b/packages/app/src/components/DBRowOverviewPanel.tsx index 55d3829c5..c9aa0959c 100644 --- a/packages/app/src/components/DBRowOverviewPanel.tsx +++ b/packages/app/src/components/DBRowOverviewPanel.tsx @@ -2,7 +2,10 @@ import { useCallback, useContext, useMemo } from 'react'; import { flatten } from 'flat'; import isString from 'lodash/isString'; import pickBy from 'lodash/pickBy'; -import { SourceKind, TSource } from '@hyperdx/common-utils/dist/types'; +import type { + TLogSource, + TTraceSource, +} from '@hyperdx/common-utils/dist/types'; import { Accordion, Box, Divider, Flex, Text } from '@mantine/core'; import { getEventBody } from '@/source'; @@ -23,7 +26,7 @@ export function RowOverviewPanel({ hideHeader = false, 'data-testid': dataTestId, }: { - source: TSource; + source: TLogSource | TTraceSource; rowId: string | undefined | null; hideHeader?: boolean; 'data-testid'?: string; diff --git a/packages/app/src/components/DBRowSidePanel.tsx b/packages/app/src/components/DBRowSidePanel.tsx index c12a895cf..8ea301717 100644 --- a/packages/app/src/components/DBRowSidePanel.tsx +++ b/packages/app/src/components/DBRowSidePanel.tsx @@ -13,7 +13,11 @@ import { parseAsStringEnum, useQueryState } from 'nuqs'; import { ErrorBoundary } from 'react-error-boundary'; import { useHotkeys } from 'react-hotkeys-hook'; import Drawer from 'react-modern-drawer'; -import { TSource } from '@hyperdx/common-utils/dist/types'; +import { + SourceKind, + type TLogSource, + type TTraceSource, +} from '@hyperdx/common-utils/dist/types'; import { ChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types'; import { Box, OptionalPortal, Stack } from '@mantine/core'; import { useClickOutside } from '@mantine/hooks'; @@ -74,7 +78,7 @@ enum Tab { } type DBRowSidePanelProps = { - source: TSource; + source: TLogSource | TTraceSource; rowId: string | undefined; onClose: () => void; isNestedPanel?: boolean; @@ -210,9 +214,9 @@ const DBRowSidePanel = ({ const traceId = normalizedRow?.['__hdx_trace_id']; const childSourceId = - source.kind === 'log' + source.kind === SourceKind.Log ? source.traceSourceId - : source.kind === 'trace' + : source.kind === SourceKind.Trace ? source.logSourceId : undefined; @@ -444,12 +448,14 @@ const DBRowSidePanel = ({ )} > - + {source.kind === SourceKind.Log && ( + + )} )} diff --git a/packages/app/src/components/DBRowTable.tsx b/packages/app/src/components/DBRowTable.tsx index 11f4b6fd5..d9d249336 100644 --- a/packages/app/src/components/DBRowTable.tsx +++ b/packages/app/src/components/DBRowTable.tsx @@ -32,7 +32,9 @@ import { import { ChartConfigWithDateRange, SelectList, - TSource, + SourceKind, + type TLogSource, + type TTraceSource, } from '@hyperdx/common-utils/dist/types'; import { splitAndTrimWithBracket } from '@hyperdx/common-utils/dist/utils'; import { @@ -348,7 +350,7 @@ export const RawLogTable = memo( loadingDate?: Date; config?: ChartConfigWithDateRange; onChildModalOpen?: (open: boolean) => void; - source?: TSource; + source?: TTraceSource | TLogSource; onExpandedRowsChange?: (hasExpandedRows: boolean) => void; collapseAllRows?: boolean; showExpandButton?: boolean; @@ -1267,13 +1269,21 @@ function DBSqlRowTableComponent({ } }, [isError, onError, error]); - const { data: source } = useSource({ id: sourceId }); + const { data: logSource } = useSource({ id: sourceId, kind: SourceKind.Log }); + const { data: traceSource } = useSource({ + id: sourceId, + kind: SourceKind.Trace, + }); + const source = logSource ?? traceSource; const patternColumn = columns[columns.length - 1]; const groupedPatterns = useGroupedPatterns({ config, samples: 10_000, bodyValueExpression: patternColumn ?? '', - severityTextExpression: source?.severityTextExpression ?? '', + severityTextExpression: + (source && 'severityTextExpression' in source + ? source.severityTextExpression + : '') ?? '', totalCount: undefined, enabled: denoiseResults, }); diff --git a/packages/app/src/components/DBSessionPanel.tsx b/packages/app/src/components/DBSessionPanel.tsx index 073aa57fe..cd9c45bfd 100644 --- a/packages/app/src/components/DBSessionPanel.tsx +++ b/packages/app/src/components/DBSessionPanel.tsx @@ -1,5 +1,6 @@ import { useMemo } from 'react'; import Link from 'next/link'; +import { SourceKind } from '@hyperdx/common-utils/dist/types'; import { Loader } from '@mantine/core'; import SessionSubpanel from '@/SessionSubpanel'; @@ -18,46 +19,48 @@ export const useSessionId = ({ dateRange: [Date, Date]; enabled?: boolean; }) => { - // trace source - const { data: source } = useSource({ id: sourceId }); + const { data: traceSource } = useSource({ + id: sourceId, + kind: SourceKind.Trace, + }); const config = useMemo(() => { - if (!source) { + if (!traceSource) { return; } return { select: [ { - valueExpression: `${source.timestampValueExpression}`, + valueExpression: `${traceSource.timestampValueExpression}`, alias: 'Timestamp', }, { - valueExpression: `${source.resourceAttributesExpression}['rum.sessionId']`, + valueExpression: `${traceSource.resourceAttributesExpression}['rum.sessionId']`, alias: 'rumSessionId', }, { - valueExpression: `${source.resourceAttributesExpression}['service.name']`, + valueExpression: `${traceSource.resourceAttributesExpression}['service.name']`, alias: 'serviceName', }, { - valueExpression: `${source.parentSpanIdExpression}`, + valueExpression: `${traceSource.parentSpanIdExpression}`, alias: 'parentSpanId', }, ], - from: source.from, - timestampValueExpression: source.timestampValueExpression, + from: traceSource.from, + timestampValueExpression: traceSource.timestampValueExpression, limit: { limit: 10000 }, - connection: source.connection, - where: `${source.traceIdExpression} = '${traceId}'`, + connection: traceSource.connection, + where: `${traceSource.traceIdExpression} = '${traceId}'`, whereLanguage: 'sql' as const, }; - }, [source, traceId]); + }, [traceSource, traceId]); const { data } = useEventsData({ config: config!, // ok to force unwrap, the query will be disabled if source is null dateRangeStartInclusive: true, dateRange, - enabled: enabled && !!source, + enabled: enabled && !!traceSource, }); const result = useMemo(() => { @@ -99,9 +102,13 @@ export const DBSessionPanel = ({ serviceName: string; setSubDrawerOpen: (open: boolean) => void; }) => { - const { data: traceSource } = useSource({ id: traceSourceId }); + const { data: traceSource } = useSource({ + id: traceSourceId, + kind: SourceKind.Trace, + }); const { data: sessionSource, isLoading: isSessionSourceLoading } = useSource({ id: traceSource?.sessionSourceId, + kind: SourceKind.Session, }); if (!traceSource || (!sessionSource && isSessionSourceLoading)) { diff --git a/packages/app/src/components/DBSqlRowTableWithSidebar.tsx b/packages/app/src/components/DBSqlRowTableWithSidebar.tsx index 7a43391b2..24820d3e6 100644 --- a/packages/app/src/components/DBSqlRowTableWithSidebar.tsx +++ b/packages/app/src/components/DBSqlRowTableWithSidebar.tsx @@ -3,7 +3,9 @@ import { useQueryState } from 'nuqs'; import { ClickHouseQueryError } from '@hyperdx/common-utils/dist/clickhouse'; import { ChartConfigWithDateRange, - TSource, + SourceKind, + type TLogSource, + type TTraceSource, } from '@hyperdx/common-utils/dist/types'; import { SortingState } from '@tanstack/react-table'; @@ -57,7 +59,15 @@ export default function DBSqlRowTableWithSideBar({ onSortingChange, initialSortBy, }: Props) { - const { data: sourceData } = useSource({ id: sourceId }); + const { data: sourceLog } = useSource({ + id: sourceId, + kind: SourceKind.Log, + }); + const { data: sourceTrace } = useSource({ + id: sourceId, + kind: SourceKind.Trace, + }); + const sourceData = sourceLog ?? sourceTrace; const [rowId, setRowId] = useQueryState('rowWhere'); const [, setRowSource] = useQueryState('rowSource'); @@ -126,7 +136,7 @@ function RowOverviewPanelWrapper({ source, rowId, }: { - source: TSource; + source: TTraceSource | TLogSource; rowId: string; }) { // Use localStorage to persist the selected tab diff --git a/packages/app/src/components/DBTableSelect.tsx b/packages/app/src/components/DBTableSelect.tsx index a9e66002d..5dc085eb8 100644 --- a/packages/app/src/components/DBTableSelect.tsx +++ b/packages/app/src/components/DBTableSelect.tsx @@ -1,4 +1,5 @@ import { useController, UseControllerProps } from 'react-hook-form'; +import { SourceKind } from '@hyperdx/common-utils/dist/types'; import { Flex, Select } from '@mantine/core'; import { useTablesDirect } from '@/clickhouse'; @@ -41,11 +42,16 @@ export default function DBTableSelect({ sourceSchemaPreview: connectionId && database && table ? ( ) : undefined, }); diff --git a/packages/app/src/components/DBTimeChart.tsx b/packages/app/src/components/DBTimeChart.tsx index b8fa4e4b4..2e54a7489 100644 --- a/packages/app/src/components/DBTimeChart.tsx +++ b/packages/app/src/components/DBTimeChart.tsx @@ -7,6 +7,7 @@ import { isMetricChartConfig } from '@hyperdx/common-utils/dist/renderChartConfi import { ChartConfigWithDateRange, DisplayType, + SourceKind, } from '@hyperdx/common-utils/dist/types'; import { Button, Code, Group, Modal, Text } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; @@ -147,7 +148,11 @@ function DBTimeChartComponent({ return null; } const isMetricChart = isMetricChartConfig(config); - if (isMetricChart && source?.logSourceId == null) { + if ( + isMetricChart && + source?.kind === SourceKind.Metric && + source?.logSourceId == null + ) { notifications.show({ color: 'yellow', message: 'No log source is associated with the selected metric source.', @@ -169,7 +174,10 @@ function DBTimeChartComponent({ whereLanguage = config.select[0].aggConditionLanguage ?? 'lucene'; } return new URLSearchParams({ - source: (isMetricChart ? source?.logSourceId : source?.id) ?? '', + source: + (source?.kind === SourceKind.Metric + ? source?.logSourceId + : source?.id) ?? '', where: where, whereLanguage: whereLanguage, filters: JSON.stringify(config.filters), diff --git a/packages/app/src/components/DBTracePanel.tsx b/packages/app/src/components/DBTracePanel.tsx index 7535df5ca..d0dd7af1e 100644 --- a/packages/app/src/components/DBTracePanel.tsx +++ b/packages/app/src/components/DBTracePanel.tsx @@ -101,14 +101,20 @@ export default function DBTracePanel({ setValue: traceIdSetValue, } = useForm<{ traceIdExpression: string }>({ defaultValues: { - traceIdExpression: parentSourceData?.traceIdExpression ?? '', + traceIdExpression: + (parentSourceData?.kind === SourceKind.Log && + parentSourceData?.traceIdExpression) || + '', }, }); useEffect(() => { - if (parentSourceData?.traceIdExpression) { + if ( + parentSourceData?.kind === SourceKind.Log && + parentSourceData?.traceIdExpression + ) { traceIdSetValue('traceIdExpression', parentSourceData.traceIdExpression); } - }, [parentSourceData?.traceIdExpression, traceIdSetValue]); + }, [parentSourceData, traceIdSetValue]); const [showTraceIdInput, setShowTraceIdInput] = useState(false); @@ -126,8 +132,10 @@ export default function DBTracePanel({ - {parentSourceData?.traceIdExpression}:{' '} - {traceId || 'No trace id found for event'} + {parentSourceData?.kind === SourceKind.Log + ? parentSourceData?.traceIdExpression + : undefined}{' '} + : {traceId || 'No trace id found for event'} {traceId != null && (