Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ The client exposes the following resource modules:
| `client.webhooks` | Outgoing webhook endpoints |
| `client.apiKeys` | API key management |
| `client.dependencies` | Service dependency tracking |
| `client.services` | Status Data catalog (third-party service status, incidents, uptime) |
| `client.deployLock` | Deploy lock for safe deployments |
| `client.statusPages` | Public status page management |
| `client.status` | Dashboard overview |
Expand Down
9 changes: 9 additions & 0 deletions docs/openapi/monitoring-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -13520,6 +13520,15 @@
"type": "boolean"
}
},
{
"name": "search",
"in": "query",
"description": "Case-insensitive substring match on service name or slug",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "cursor",
"in": "query",
Expand Down
3 changes: 3 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {ResourceGroups} from './resources/resource-groups.js'
import {Webhooks} from './resources/webhooks.js'
import {ApiKeys} from './resources/api-keys.js'
import {Dependencies} from './resources/dependencies.js'
import {Services} from './resources/services.js'
import {DeployLock} from './resources/deploy-lock.js'
import {Status} from './resources/status.js'
import {StatusPages} from './resources/status-pages.js'
Expand Down Expand Up @@ -48,6 +49,7 @@ export class Devhelm {
readonly webhooks: Webhooks
readonly apiKeys: ApiKeys
readonly dependencies: Dependencies
readonly services: Services
readonly deployLock: DeployLock
readonly status: Status
readonly statusPages: StatusPages
Expand All @@ -67,6 +69,7 @@ export class Devhelm {
this.webhooks = new Webhooks(client)
this.apiKeys = new ApiKeys(client)
this.dependencies = new Dependencies(client)
this.services = new Services(client)
this.deployLock = new DeployLock(client)
this.status = new Status(client)
this.statusPages = new StatusPages(client)
Expand Down
2 changes: 2 additions & 0 deletions src/generated/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18741,6 +18741,8 @@ export interface operations {
status?: string;
/** @description Filter by published status for pSEO pages */
published?: boolean;
/** @description Case-insensitive substring match on service name or slug */
search?: string;
/** @description Opaque cursor from a previous response */
cursor?: string;
/** @description Page size (1–100, default 20) */
Expand Down
23 changes: 23 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,25 @@ export type {
AssertionTestResultDto,
MonitorTestResultDto,
MaintenanceWindowDto,
ServiceCatalogDto,
ServiceDetailDto,
ServiceLiveStatusDto,
ServiceStatusDto,
CategoryDto,
GlobalStatusSummaryDto,
ServiceComponentDto,
ComponentStatusDto,
ComponentsSummaryDto,
ComponentUptimeSummaryDto,
ComponentUptimeDayDto,
BatchComponentUptimeDto,
ServiceDayDetailDto,
ServiceIncidentDto,
ServiceIncidentDetailDto,
ServiceIncidentUpdateDto,
ServiceUptimeResponse,
UptimeBucketDto,
ScheduledMaintenanceDto,
IncidentTimelineDto,
CheckTraceDto,
PolicySnapshotDto,
Expand Down Expand Up @@ -63,6 +82,8 @@ export type {
AcquireDeployLockRequest,
CreateMaintenanceWindowRequest,
UpdateMaintenanceWindowRequest,
ServiceSubscribeRequest,
UpdateAlertSensitivityRequest,
StatusPageDto,
StatusPageComponentDto,
StatusPageComponentGroupDto,
Expand Down Expand Up @@ -109,6 +130,8 @@ export {ResourceGroups} from './resources/resource-groups.js'
export {Webhooks} from './resources/webhooks.js'
export {ApiKeys} from './resources/api-keys.js'
export {Dependencies} from './resources/dependencies.js'
export {Services} from './resources/services.js'
export type {ServiceListFilters, ServiceIncidentFilters, UptimeRangeOptions} from './resources/services.js'
export {DeployLock} from './resources/deploy-lock.js'
export {Status} from './resources/status.js'
export {StatusPages} from './resources/status-pages.js'
Expand Down
44 changes: 37 additions & 7 deletions src/resources/dependencies.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import type {ApiClient} from '../http.js'
import type {ServiceSubscriptionDto, Page} from '../types.js'
import {ServiceSubscriptionDtoSchema} from '../schemas.js'
import type {ServiceSubscriptionDto, ServiceSubscribeRequest, Page} from '../types.js'
import {
ServiceSubscriptionDtoSchema,
ServiceSubscribeRequestSchema,
UpdateAlertSensitivityRequestSchema,
} from '../schemas.js'
import {fetchAllPages, fetchPage, fetchSingle, fetchVoid} from '../http.js'
import {validateRequest} from '../validation.js'

export class Dependencies {
constructor(private readonly client: ApiClient) {}
Expand All @@ -24,12 +29,37 @@ export class Dependencies {
/**
* Track (subscribe to) a service from the catalog by its slug.
*
* The endpoint takes no request body per the OpenAPI spec — the slug is
* the entire payload. If a body is added upstream, wire `validateRequest`
* here against the new schema before sending.
* Optionally scope the subscription to a single component (`componentId`)
* and/or set the alert sensitivity. When neither option is provided the
* request is sent without a body, matching the server's defaults
* (whole-service subscription, default sensitivity).
*/
async track(slug: string): Promise<ServiceSubscriptionDto> {
return fetchSingle(this.client, 'POST', `/api/v1/service-subscriptions/${slug}`, ServiceSubscriptionDtoSchema)
async track(slug: string, options?: ServiceSubscribeRequest): Promise<ServiceSubscriptionDto> {
const hasBody = options !== undefined && (options.componentId !== undefined || options.alertSensitivity !== undefined)
if (hasBody) validateRequest(ServiceSubscribeRequestSchema, options, 'dependencies.track')
return fetchSingle(
this.client,
'POST',
`/api/v1/service-subscriptions/${slug}`,
ServiceSubscriptionDtoSchema,
hasBody ? options : undefined,
)
}

/** Change how sensitively a subscription alerts (e.g. `ALL`, `MAJOR_ONLY`). */
async updateAlertSensitivity(
subscriptionId: string | number,
alertSensitivity: string,
): Promise<ServiceSubscriptionDto> {
const body = {alertSensitivity}
validateRequest(UpdateAlertSensitivityRequestSchema, body, 'dependencies.updateAlertSensitivity')
return fetchSingle(
this.client,
'PATCH',
`/api/v1/service-subscriptions/${subscriptionId}/alert-sensitivity`,
ServiceSubscriptionDtoSchema,
body,
)
}

/** Untrack (unsubscribe from) a service subscription. */
Expand Down
231 changes: 231 additions & 0 deletions src/resources/services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import type {ApiClient} from '../http.js'
import type {
ServiceCatalogDto,
ServiceDetailDto,
ServiceLiveStatusDto,
CategoryDto,
GlobalStatusSummaryDto,
ServiceComponentDto,
ComponentUptimeDayDto,
BatchComponentUptimeDto,
ServiceDayDetailDto,
ServiceIncidentDto,
ServiceIncidentDetailDto,
ServiceUptimeResponse,
ScheduledMaintenanceDto,
Page,
CursorPage,
} from '../types.js'
import {
ServiceCatalogDtoSchema,
ServiceDetailDtoSchema,
ServiceLiveStatusDtoSchema,
CategoryDtoSchema,
GlobalStatusSummaryDtoSchema,
ServiceComponentDtoSchema,
ComponentUptimeDayDtoSchema,
BatchComponentUptimeDtoSchema,
ServiceDayDetailDtoSchema,
ServiceIncidentDtoSchema,
ServiceIncidentDetailDtoSchema,
ServiceUptimeResponseSchema,
ScheduledMaintenanceDtoSchema,
} from '../schemas.js'
import {apiGet, fetchSingle} from '../http.js'
import {parsePage, parseCursorPage} from '../validation.js'

const BASE = '/api/v1/services'

/** Filters for listing catalog services. */
export interface ServiceListFilters {
/** Filter by category slug (see `client.services.categories()`). */
category?: string
/** Filter by current overall status (e.g. `OPERATIONAL`, `MAJOR_OUTAGE`). */
status?: string
/** Free-text search over service name and slug. */
search?: string
/** Opaque cursor from a previous page's `nextCursor`. */
cursor?: string
/** Page size. */
limit?: number
}

/** Filters for listing service incidents (per-service or global). */
export interface ServiceIncidentFilters {
/** When set, lists incidents for this service only; otherwise lists across all services. */
slugOrId?: string
/** Filter by incident status (e.g. `investigating`, `resolved`). */
status?: string
/** Only incidents that started at or after this ISO 8601 timestamp. */
from?: string
/** Filter by service category (global mode only). */
category?: string
page?: number
size?: number
}

/** Time-range options for component uptime queries. */
export interface UptimeRangeOptions {
/** Named period (e.g. `30d`, `90d`). Mutually exclusive with from/to. */
period?: string
/** Range start, ISO 8601. */
from?: string
/** Range end, ISO 8601. */
to?: string
}

/**
* Read-only Status Data catalog — third-party services DevHelm polls
* (their live status, components, incidents, uptime, and maintenances).
*
* To subscribe your org to a service for alerting, see
* `client.dependencies.track()`.
*/
export class Services {
constructor(private readonly client: ApiClient) {}

/** List catalog services (cursor-paginated). */
async list(options: ServiceListFilters = {}): Promise<CursorPage<ServiceCatalogDto>> {
const query: Record<string, unknown> = {}
if (options.category !== undefined) query['category'] = options.category
if (options.status !== undefined) query['status'] = options.status
if (options.search !== undefined) query['search'] = options.search
if (options.cursor !== undefined) query['cursor'] = options.cursor
if (options.limit !== undefined) query['limit'] = options.limit

const raw = await apiGet(this.client, BASE, query)
const validated = parseCursorPage(ServiceCatalogDtoSchema, raw, BASE)
return {
data: validated.data,
nextCursor: validated.nextCursor ?? null,
hasMore: validated.hasMore,
}
}

/** Get a single service by slug or UUID. Set `summary` to omit heavy detail sections. */
async get(slugOrId: string, options: {summary?: boolean} = {}): Promise<ServiceDetailDto> {
const query: Record<string, unknown> = {}
if (options.summary !== undefined) query['summary'] = options.summary
return fetchSingle(this.client, 'GET', `${BASE}/${slugOrId}`, ServiceDetailDtoSchema, undefined, query)
}

/** Current live status of a service (overall + per-component). */
async liveStatus(slugOrId: string): Promise<ServiceLiveStatusDto> {
return fetchSingle(this.client, 'GET', `${BASE}/${slugOrId}/live-status`, ServiceLiveStatusDtoSchema)
}

/** List catalog categories with per-category service counts. */
async categories(): Promise<CategoryDto[]> {
const path = '/api/v1/categories'
const raw = await apiGet(this.client, path)
return parsePage(CategoryDtoSchema, raw, path).data
}

/** Global status summary across the whole catalog. */
async summary(): Promise<GlobalStatusSummaryDto> {
return fetchSingle(this.client, 'GET', `${BASE}/summary`, GlobalStatusSummaryDtoSchema)
}

/** List components of a service, optionally scoped to a component group. */
async components(slugOrId: string, options: {groupId?: string} = {}): Promise<ServiceComponentDto[]> {
const path = `${BASE}/${slugOrId}/components`
const query: Record<string, unknown> = {}
if (options.groupId !== undefined) query['groupId'] = options.groupId
const raw = await apiGet(this.client, path, query)
return parsePage(ServiceComponentDtoSchema, raw, path).data
}

/** Daily uptime history for a single component. */
async componentUptime(
slugOrId: string,
componentId: string,
options: UptimeRangeOptions = {},
): Promise<ComponentUptimeDayDto[]> {
const path = `${BASE}/${slugOrId}/components/${componentId}/uptime`
const raw = await apiGet(this.client, path, buildRangeQuery(options))
return parsePage(ComponentUptimeDayDtoSchema, raw, path).data
}

/** Daily uptime history for every component of a service, keyed by component id. */
async batchComponentUptime(slugOrId: string, options: UptimeRangeOptions = {}): Promise<BatchComponentUptimeDto> {
return fetchSingle(
this.client,
'GET',
`${BASE}/${slugOrId}/components/uptime`,
BatchComponentUptimeDtoSchema,
undefined,
buildRangeQuery(options),
)
}

/** Detailed breakdown of a single day (`date` as `YYYY-MM-DD`). */
async day(slugOrId: string, date: string): Promise<ServiceDayDetailDto> {
return fetchSingle(this.client, 'GET', `${BASE}/${slugOrId}/days/${date}`, ServiceDayDetailDtoSchema)
}

/**
* List service incidents (paginated).
*
* With `slugOrId` set, lists incidents for that service; without it,
* lists incidents across the whole catalog (optionally filtered by
* `category`, which only applies in global mode).
*/
async incidents(options: ServiceIncidentFilters = {}): Promise<Page<ServiceIncidentDto>> {
const path = options.slugOrId !== undefined ? `${BASE}/${options.slugOrId}/incidents` : `${BASE}/incidents`
const query: Record<string, unknown> = {
page: options.page ?? 0,
size: options.size ?? 20,
}
if (options.status !== undefined) query['status'] = options.status
if (options.from !== undefined) query['from'] = options.from
if (options.slugOrId === undefined && options.category !== undefined) query['category'] = options.category

const raw = await apiGet(this.client, path, query)
const validated = parsePage(ServiceIncidentDtoSchema, raw, path)
return {
data: validated.data,
hasNext: validated.hasNext,
hasPrev: validated.hasPrev,
totalElements: validated.totalElements ?? null,
totalPages: validated.totalPages ?? null,
}
}

/** Get a single service incident with its update timeline. */
async incident(slugOrId: string, incidentId: string): Promise<ServiceIncidentDetailDto> {
return fetchSingle(
this.client,
'GET',
`${BASE}/${slugOrId}/incidents/${incidentId}`,
ServiceIncidentDetailDtoSchema,
)
}

/** Service-level uptime over a period, bucketed by granularity. */
async uptime(
slugOrId: string,
options: {period?: string; granularity?: string} = {},
): Promise<ServiceUptimeResponse> {
const query: Record<string, unknown> = {}
if (options.period !== undefined) query['period'] = options.period
if (options.granularity !== undefined) query['granularity'] = options.granularity
return fetchSingle(this.client, 'GET', `${BASE}/${slugOrId}/uptime`, ServiceUptimeResponseSchema, undefined, query)
}

/** List scheduled maintenances for a service, optionally filtered by status(es). */
async maintenances(slugOrId: string, options: {status?: string | string[]} = {}): Promise<ScheduledMaintenanceDto[]> {
const path = `${BASE}/${slugOrId}/maintenances`
const query: Record<string, unknown> = {}
if (options.status !== undefined) query['status'] = options.status
const raw = await apiGet(this.client, path, query)
return parsePage(ScheduledMaintenanceDtoSchema, raw, path).data
}
}

function buildRangeQuery(options: UptimeRangeOptions): Record<string, unknown> {
const query: Record<string, unknown> = {}
if (options.period !== undefined) query['period'] = options.period
if (options.from !== undefined) query['from'] = options.from
if (options.to !== undefined) query['to'] = options.to
return query
}
Loading
Loading