Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ export const myEndpoint = defineEndpoint("my_endpoint", {

Never use raw `fetch()` calls to `/v0/sql` - always define typed endpoints for type safety and consistency.

When adding a new Tinybird endpoint, register it in **three** places:
1. Define in `packages/domain/src/tinybird/endpoints.ts`
2. Add pipe name to `packages/domain/src/tinybird-pipes.ts`
3. Register in `apps/api/src/services/TinybirdService.ts` pipes config + import

## Environment Variables

```
Expand Down
4 changes: 4 additions & 0 deletions apps/api/src/services/TinybirdService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
errorsFacets,
errorsSummary,
getServiceUsage,
httpEndpointsOverview,
httpEndpointsTimeseries,
listLogs,
listMetrics,
listTraces,
Expand Down Expand Up @@ -99,6 +101,8 @@ export class TinybirdService extends Effect.Service<TinybirdService>()("Tinybird
span_attribute_values: spanAttributeValues,
resource_attribute_keys: resourceAttributeKeys,
resource_attribute_values: resourceAttributeValues,
http_endpoints_overview: httpEndpointsOverview,
http_endpoints_timeseries: httpEndpointsTimeseries,
},
})

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/api/tinybird/custom-charts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function sortByBucket<T extends { bucket: string }>(rows: T[]): T[] {
return [...rows].sort((left, right) => left.bucket.localeCompare(right.bucket))
}

function fillServiceDetailPoints(
export function fillServiceDetailPoints(
points: ServiceDetailTimeSeriesPoint[],
startTime: string | undefined,
endTime: string | undefined,
Expand Down
136 changes: 136 additions & 0 deletions apps/web/src/api/tinybird/endpoint-detail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { Effect, Schema } from "effect"
import { getTinybird } from "@/lib/tinybird"
import {
computeBucketSeconds,
toIsoBucket,
} from "@/api/tinybird/timeseries-utils"
import {
TinybirdDateTimeString,
decodeInput,
runTinybirdQuery,
} from "@/api/tinybird/effect-utils"
import { fillServiceDetailPoints } from "@/api/tinybird/custom-charts"
import type { ServiceDetailTimeSeriesPoint } from "@/api/tinybird/services"

const GetEndpointDetailTimeSeriesInputSchema = Schema.Struct({
serviceName: Schema.String,
spanName: Schema.String,
startTime: Schema.optional(TinybirdDateTimeString),
endTime: Schema.optional(TinybirdDateTimeString),
})

export type GetEndpointDetailTimeSeriesInput = Schema.Schema.Type<
typeof GetEndpointDetailTimeSeriesInputSchema
>

export function getEndpointDetailTimeSeries({
data,
}: {
data: GetEndpointDetailTimeSeriesInput
}) {
return getEndpointDetailTimeSeriesEffect({ data })
}

const getEndpointDetailTimeSeriesEffect = Effect.fn(
"Tinybird.getEndpointDetailTimeSeries",
)(function* ({
data,
}: {
data: GetEndpointDetailTimeSeriesInput
}) {
const input = yield* decodeInput(
GetEndpointDetailTimeSeriesInputSchema,
data,
"getEndpointDetailTimeSeries",
)

const tinybird = getTinybird()
const bucketSeconds = computeBucketSeconds(input.startTime, input.endTime)
const result = yield* runTinybirdQuery("custom_traces_timeseries", () =>
tinybird.query.custom_traces_timeseries({
start_time: input.startTime,
end_time: input.endTime,
bucket_seconds: bucketSeconds,
service_name: input.serviceName,
span_name: input.spanName,
}),
)

const points = result.data.map(
(row): ServiceDetailTimeSeriesPoint => ({
bucket: toIsoBucket(row.bucket),
throughput: Number(row.count),
errorRate: Number(row.errorRate),
p50LatencyMs: Number(row.p50Duration),
p95LatencyMs: Number(row.p95Duration),
p99LatencyMs: Number(row.p99Duration),
}),
)

return {
data: fillServiceDetailPoints(
points,
input.startTime,
input.endTime,
bucketSeconds,
),
}
})

const GetEndpointStatusCodeBreakdownInputSchema = Schema.Struct({
serviceName: Schema.String,
spanName: Schema.String,
startTime: Schema.optional(TinybirdDateTimeString),
endTime: Schema.optional(TinybirdDateTimeString),
})

export type GetEndpointStatusCodeBreakdownInput = Schema.Schema.Type<
typeof GetEndpointStatusCodeBreakdownInputSchema
>

export interface StatusCodeBreakdownItem {
statusCode: string
count: number
}

export function getEndpointStatusCodeBreakdown({
data,
}: {
data: GetEndpointStatusCodeBreakdownInput
}) {
return getEndpointStatusCodeBreakdownEffect({ data })
}

const getEndpointStatusCodeBreakdownEffect = Effect.fn(
"Tinybird.getEndpointStatusCodeBreakdown",
)(function* ({
data,
}: {
data: GetEndpointStatusCodeBreakdownInput
}) {
const input = yield* decodeInput(
GetEndpointStatusCodeBreakdownInputSchema,
data,
"getEndpointStatusCodeBreakdown",
)

const tinybird = getTinybird()
const result = yield* runTinybirdQuery("custom_traces_breakdown", () =>
tinybird.query.custom_traces_breakdown({
start_time: input.startTime,
end_time: input.endTime,
service_name: input.serviceName,
span_name: input.spanName,
group_by_attribute: "http.status_code",
}),
)

return {
data: result.data.map(
(row): StatusCodeBreakdownItem => ({
statusCode: String(row.name),
count: Number(row.count),
}),
),
}
})
188 changes: 188 additions & 0 deletions apps/web/src/api/tinybird/endpoints-overview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { Effect, Schema } from "effect"
import { getTinybird, type HttpEndpointsOverviewOutput } from "@/lib/tinybird"
import {
TinybirdDateTimeString,
decodeInput,
runTinybirdQuery,
} from "@/api/tinybird/effect-utils"
import {
buildBucketTimeline,
computeBucketSeconds,
toIsoBucket,
} from "@/api/tinybird/timeseries-utils"

const dateTimeString = TinybirdDateTimeString

export interface HttpEndpointOverview {
serviceName: string
endpointName: string
httpMethod: string
count: number
avgDuration: number
p50Duration: number
p95Duration: number
p99Duration: number
errorRate: number
}

export interface HttpEndpointsOverviewResponse {
data: HttpEndpointOverview[]
}

const GetHttpEndpointsOverviewInput = Schema.Struct({
startTime: dateTimeString,
endTime: dateTimeString,
services: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
httpMethods: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
environments: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
})

export type GetHttpEndpointsOverviewInput = Schema.Schema.Type<typeof GetHttpEndpointsOverviewInput>

function coerceRow(raw: HttpEndpointsOverviewOutput): HttpEndpointOverview {
return {
serviceName: raw.serviceName,
endpointName: raw.endpointName,
httpMethod: raw.httpMethod || "UNKNOWN",
count: Number(raw.count),
avgDuration: Number(raw.avgDuration),
p50Duration: Number(raw.p50Duration),
p95Duration: Number(raw.p95Duration),
p99Duration: Number(raw.p99Duration),
errorRate: Number(raw.errorRate),
}
}

export function getHttpEndpointsOverview({
data,
}: {
data: GetHttpEndpointsOverviewInput
}) {
return getHttpEndpointsOverviewEffect({ data })
}

const getHttpEndpointsOverviewEffect = Effect.fn("Tinybird.getHttpEndpointsOverview")(
function* ({
data,
}: {
data: GetHttpEndpointsOverviewInput
}) {
const input = yield* decodeInput(
GetHttpEndpointsOverviewInput,
data,
"getHttpEndpointsOverview",
)

const tinybird = getTinybird()
const result = yield* runTinybirdQuery("http_endpoints_overview", () =>
tinybird.query.http_endpoints_overview({
start_time: input.startTime,
end_time: input.endTime,
services: input.services?.join(","),
http_methods: input.httpMethods?.join(","),
environments: input.environments?.join(","),
}),
)

return {
data: result.data.map(coerceRow),
}
},
)

// Sparkline types
export interface EndpointSparklinePoint {
bucket: string
throughput: number
errorRate: number
}

const GetHttpEndpointsSparklinesInput = Schema.Struct({
startTime: Schema.optional(dateTimeString),
endTime: Schema.optional(dateTimeString),
services: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
httpMethods: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
environments: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
})

export type GetHttpEndpointsSparklinesInput = Schema.Schema.Type<typeof GetHttpEndpointsSparklinesInput>

function fillSparklinePoints(
points: EndpointSparklinePoint[],
timeline: string[],
): EndpointSparklinePoint[] {
if (timeline.length === 0) {
return [...points].sort((a, b) => a.bucket.localeCompare(b.bucket))
}

const byBucket = new Map<string, EndpointSparklinePoint>()
for (const point of points) {
byBucket.set(toIsoBucket(point.bucket), point)
}

return timeline.map((bucket) => {
const existing = byBucket.get(bucket)
if (existing) return existing
return { bucket, throughput: 0, errorRate: 0 }
})
}

export function getHttpEndpointsSparklines({
data,
}: {
data: GetHttpEndpointsSparklinesInput
}) {
return getHttpEndpointsSparklinesEffect({ data })
}

const getHttpEndpointsSparklinesEffect = Effect.fn("Tinybird.getHttpEndpointsSparklines")(
function* ({
data,
}: {
data: GetHttpEndpointsSparklinesInput
}) {
const input = yield* decodeInput(
GetHttpEndpointsSparklinesInput,
data ?? {},
"getHttpEndpointsSparklines",
)

const tinybird = getTinybird()
const bucketSeconds = computeBucketSeconds(input.startTime, input.endTime)
const result = yield* runTinybirdQuery("http_endpoints_timeseries", () =>
tinybird.query.http_endpoints_timeseries({
start_time: input.startTime,
end_time: input.endTime,
bucket_seconds: bucketSeconds,
services: input.services?.join(","),
http_methods: input.httpMethods?.join(","),
environments: input.environments?.join(","),
}),
)

const timeline = buildBucketTimeline(input.startTime, input.endTime, bucketSeconds)
// endpointKey format: "serviceName::endpointName::httpMethod"
const grouped: Record<string, EndpointSparklinePoint[]> = {}
for (const row of result.data) {
const bucket = toIsoBucket(row.bucket)
const point: EndpointSparklinePoint = {
bucket,
throughput: Number(row.count),
errorRate: Number(row.errorRate),
}
if (!grouped[row.endpointKey]) {
grouped[row.endpointKey] = []
}
grouped[row.endpointKey].push(point)
}

const filledGrouped = Object.fromEntries(
Object.entries(grouped).map(([key, points]) => [
key,
fillSparklinePoints(points, timeline),
]),
)

return { data: filledGrouped }
},
)
6 changes: 6 additions & 0 deletions apps/web/src/components/dashboard/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
HouseIcon,
FileIcon,
PulseIcon,
ChartBarIcon,
ChartLineIcon,
ServerIcon,
CircleWarningIcon,
Expand Down Expand Up @@ -69,6 +70,11 @@ const servicesNavItems = [
]

const telemetryNavItems = [
{
title: "Endpoints",
href: "/endpoints",
icon: ChartBarIcon,
},
{
title: "Errors",
href: "/errors",
Expand Down
Loading