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
4 changes: 4 additions & 0 deletions packages/cli/src/commands/authCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import prompts from 'prompts'
import { BaseCommand } from './baseCommand.js'
import * as api from '../rest/api.js'
import { Account } from '../rest/accounts.js'
import { Session } from '../constructs/session.js'
import { detectCliMode } from '../helpers/cli-mode.js'
import type { CommandPreview } from '../helpers/command-preview.js'
import { formatPreviewForAgent, formatPreviewForTerminal } from '../helpers/command-preview.js'
Expand All @@ -22,6 +23,9 @@ export abstract class AuthCommand extends BaseCommand {
protected async init (): Promise<any> {
await super.init()
this.#account = await api.validateAuthentication()
// Constructs validate against account-specific limits and have no access to
// the command instance.
Session.accountFeatures = this.#account?.features ?? []
}

protected async confirmOrAbort (
Expand Down
122 changes: 122 additions & 0 deletions packages/cli/src/constructs/__tests__/account-features.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { describe, it, expect, afterEach } from 'vitest'

import { ApiCheck, TcpMonitor, TcpRequest, UrlMonitor } from '../index.js'
import { Diagnostics } from '../diagnostics.js'
import { Project } from '../project.js'
import { Session } from '../session.js'

const EXTENDED_RESPONSE_TIME_LIMITS = 'EXTENDED_RESPONSE_TIME_LIMITS'

const tcpRequest: TcpRequest = {
hostname: 'acme.com',
port: 443,
}

const newProject = () => {
Session.project = new Project('project-id', {
name: 'Test Project',
repoUrl: 'https://github.com/checkly/checkly-cli',
})
}

const validate = async (construct: { validate: (diagnostics: Diagnostics) => Promise<void> }) => {
const diagnostics = new Diagnostics()
await construct.validate(diagnostics)
return diagnostics
}

describe('account-aware response time limits', () => {
afterEach(() => {
Session.accountFeatures = []
})

it('rejects an extended response time without the feature', async () => {
newProject()
Session.accountFeatures = []

const diagnostics = await validate(new TcpMonitor('tcp-standard', {
name: 'Test Check',
request: tcpRequest,
maxResponseTime: 45_000,
}))

expect(diagnostics.isFatal()).toBe(true)
expect(diagnostics.observations).toEqual(expect.arrayContaining([
expect.objectContaining({
message: expect.stringContaining('must be 5000 or lower'),
}),
]))
})

it('accepts an extended response time with the feature', async () => {
newProject()
Session.accountFeatures = [EXTENDED_RESPONSE_TIME_LIMITS]

const diagnostics = await validate(new TcpMonitor('tcp-extended', {
name: 'Test Check',
request: tcpRequest,
degradedResponseTime: 40_000,
maxResponseTime: 45_000,
}))

expect(diagnostics.isFatal()).toBe(false)
})

it('defers values above 45s to the API with the feature', async () => {
newProject()
Session.accountFeatures = [EXTENDED_RESPONSE_TIME_LIMITS]

const diagnostics = await validate(new TcpMonitor('tcp-over-extended', {
name: 'Test Check',
request: tcpRequest,
maxResponseTime: 300_000,
}))

expect(diagnostics.isFatal()).toBe(false)
})

it('keeps the degraded <= max cross-check with the feature', async () => {
newProject()
Session.accountFeatures = [EXTENDED_RESPONSE_TIME_LIMITS]

const diagnostics = await validate(new TcpMonitor('tcp-degraded-above-max', {
name: 'Test Check',
request: tcpRequest,
degradedResponseTime: 46_000,
maxResponseTime: 45_000,
}))

expect(diagnostics.isFatal()).toBe(true)
expect(diagnostics.observations).toEqual(expect.arrayContaining([
expect.objectContaining({
message: expect.stringContaining('must be less than or equal to "maxResponseTime"'),
}),
]))
})

it('raises the API check limit above its 30s standard', async () => {
newProject()
Session.accountFeatures = [EXTENDED_RESPONSE_TIME_LIMITS]

const diagnostics = await validate(new ApiCheck('api-extended', {
name: 'Test Check',
request: { url: 'https://acme.com', method: 'GET' },
maxResponseTime: 45_000,
}))

expect(diagnostics.isFatal()).toBe(false)
})

it('raises the URL monitor limit above its 30s standard', async () => {
newProject()
Session.accountFeatures = [EXTENDED_RESPONSE_TIME_LIMITS]

const diagnostics = await validate(new UrlMonitor('url-extended', {
name: 'Test Check',
request: { url: 'https://acme.com' },
maxResponseTime: 45_000,
}))

expect(diagnostics.isFatal()).toBe(false)
})
})
6 changes: 2 additions & 4 deletions packages/cli/src/constructs/api-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Diagnostics } from './diagnostics.js'
import { DeprecatedPropertyDiagnostic, InvalidPropertyValueDiagnostic } from './construct-diagnostics.js'
import { ApiCheckBundle, ApiCheckBundleProps } from './api-check-bundle.js'
import { Assertion } from './api-assertion.js'
import { responseTimeLimits } from './internal/account-features.js'
import { validateResponseTimes } from './internal/common-diagnostics.js'
import { Bundler } from '../services/check-parser/bundler.js'

Expand Down Expand Up @@ -257,10 +258,7 @@ export class ApiCheck extends RuntimeCheck {
}
}

await validateResponseTimes(diagnostics, this, {
degradedResponseTime: 30_000,
maxResponseTime: 30_000,
})
await validateResponseTimes(diagnostics, this, responseTimeLimits(30_000))
}

async bundle (bundler: Bundler): Promise<ApiCheckBundle> {
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/constructs/dns-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Diagnostics } from './diagnostics.js'
import { validateResponseTimes } from './internal/common-diagnostics.js'
import { DnsRequest } from './dns-request.js'
import { RequiredPropertyDiagnostic } from './construct-diagnostics.js'
import { responseTimeLimits } from './internal/account-features.js'

export interface DnsMonitorProps extends MonitorProps {
/**
Expand Down Expand Up @@ -99,8 +100,7 @@ export class DnsMonitor extends Monitor {
}

await validateResponseTimes(diagnostics, this, {
degradedResponseTime: 5_000,
maxResponseTime: 5_000,
...responseTimeLimits(5_000),
})
}

Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/constructs/grpc-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Diagnostics } from './diagnostics.js'
import { validateResponseTimes } from './internal/common-diagnostics.js'
import { validateGrpcAssertion } from './grpc-assertion-validation.js'
import { GrpcRequest } from './grpc-request.js'
import { responseTimeLimits } from './internal/account-features.js'

export interface GrpcMonitorProps extends MonitorProps {
/**
Expand Down Expand Up @@ -79,8 +80,7 @@ export class GrpcMonitor extends Monitor {
await validateResponseTimes(diagnostics, this, {
// gRPC allows thresholds up to 180s (calls can run to the 180s timeout),
// matching the backend's gRPC response-time limits.
degradedResponseTime: 180_000,
maxResponseTime: 180_000,
...responseTimeLimits(180_000),
// Backend default applied when maxResponseTime is omitted.
defaultMaxResponseTime: 20_000,
})
Expand Down
27 changes: 27 additions & 0 deletions packages/cli/src/constructs/internal/account-features.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Session } from '../session.js'
import type { ResponseTimeLimits } from './common-diagnostics.js'

// Raises the response time limits the API accepts for TCP, API and URL checks.
const EXTENDED_RESPONSE_TIME_LIMITS = 'EXTENDED_RESPONSE_TIME_LIMITS'

/**
* Response time limits for the authenticated account.
*
* Accounts with extended limits skip the client-side cap entirely: the exact
* limit lives server-side and can change without a CLI release, so the CLI
* defers to the API instead of hardcoding a ceiling. The degraded <= max
* cross-check still applies.
*
* Falls back to the standard limit when the account is not entitled, and when
* the API does not report features at all (older or self-hosted deployments).
*/
export function responseTimeLimits (standard: number): ResponseTimeLimits {
const limit = Session.accountFeatures.includes(EXTENDED_RESPONSE_TIME_LIMITS)
? Number.MAX_SAFE_INTEGER
: standard

return {
degradedResponseTime: limit,
maxResponseTime: limit,
}
}
2 changes: 1 addition & 1 deletion packages/cli/src/constructs/internal/common-diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ type ResponseTimeProps = {
maxResponseTime?: number
}

type ResponseTimeLimits = {
export type ResponseTimeLimits = {
degradedResponseTime: number
maxResponseTime: number
// The per-type default that the backend applies to `maxResponseTime` when the
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/constructs/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export class Session {
static verifyRuntimeDependencies = true
static loadingChecklyConfigFile: boolean
static checklyConfigFileConstructs?: Construct[]
static accountFeatures: string[] = []
static privateLocations: PrivateLocationApi[]
static parsers = new Map<string, Parser>()
static playwrightProjectBundler?: PlaywrightProjectBundler
Expand All @@ -91,6 +92,7 @@ export class Session {
this.verifyRuntimeDependencies = true
this.loadingChecklyConfigFile = false
this.checklyConfigFileConstructs = undefined
this.accountFeatures = []
this.privateLocations = []
this.parsers = new Map<string, Parser>()
this.playwrightProjectBundler = undefined
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/constructs/ssl-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SslRequest } from './ssl-request.js'
import { validateResponseTimes } from './internal/common-diagnostics.js'
import { validateSslAssertion } from './ssl-assertion-validation.js'
import { RequiredPropertyDiagnostic } from './construct-diagnostics.js'
import { responseTimeLimits } from './internal/account-features.js'

export interface SslMonitorProps extends MonitorProps {
/**
Expand Down Expand Up @@ -81,8 +82,7 @@ export class SslMonitor extends Monitor {
}

await validateResponseTimes(diagnostics, this, {
degradedResponseTime: 30_000,
maxResponseTime: 30_000,
...responseTimeLimits(30_000),
// Backend default applied when maxResponseTime is omitted.
defaultMaxResponseTime: 10_000,
})
Expand Down
6 changes: 2 additions & 4 deletions packages/cli/src/constructs/tcp-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IPFamily } from './ip.js'
import { Session } from './session.js'
import { Assertion as CoreAssertion, NumericAssertionBuilder, GeneralAssertionBuilder } from './internal/assertion.js'
import { Diagnostics } from './diagnostics.js'
import { responseTimeLimits } from './internal/account-features.js'
import { validateResponseTimes } from './internal/common-diagnostics.js'

type TcpAssertionSource = 'RESPONSE_DATA' | 'RESPONSE_TIME'
Expand Down Expand Up @@ -158,10 +159,7 @@ export class TcpMonitor extends Monitor {
async validate (diagnostics: Diagnostics): Promise<void> {
await super.validate(diagnostics)

await validateResponseTimes(diagnostics, this, {
degradedResponseTime: 5_000,
maxResponseTime: 5_000,
})
await validateResponseTimes(diagnostics, this, responseTimeLimits(5_000))
}

synthesize () {
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/constructs/traceroute-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Diagnostics } from './diagnostics.js'
import { validateResponseTimes } from './internal/common-diagnostics.js'
import { validateTracerouteAssertion } from './traceroute-assertion-validation.js'
import { TracerouteRequest } from './traceroute-request.js'
import { responseTimeLimits } from './internal/account-features.js'

export interface TracerouteMonitorProps extends MonitorProps {
/**
Expand Down Expand Up @@ -77,8 +78,7 @@ export class TracerouteMonitor extends Monitor {
await super.validate(diagnostics)

await validateResponseTimes(diagnostics, this, {
degradedResponseTime: 30_000,
maxResponseTime: 30_000,
...responseTimeLimits(30_000),
// Backend default applied when maxResponseTime is omitted.
defaultMaxResponseTime: 20_000,
})
Expand Down
6 changes: 2 additions & 4 deletions packages/cli/src/constructs/url-monitor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Diagnostics } from './diagnostics.js'
import { responseTimeLimits } from './internal/account-features.js'
import { validateResponseTimes } from './internal/common-diagnostics.js'
import { Monitor, MonitorProps } from './monitor.js'
import { Session } from './session.js'
Expand Down Expand Up @@ -139,10 +140,7 @@ export class UrlMonitor extends Monitor {
async validate (diagnostics: Diagnostics): Promise<void> {
await super.validate(diagnostics)

await validateResponseTimes(diagnostics, this, {
degradedResponseTime: 30_000,
maxResponseTime: 30_000,
})
await validateResponseTimes(diagnostics, this, responseTimeLimits(30_000))
}

synthesize () {
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/rest/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ export interface Account {
plan?: string
planDisplayName?: string
addons?: Record<string, AddonTier>
/**
* Client-visible account feature flags. Absent when the API predates them,
* in which case standard limits apply.
*/
features?: string[]
}

class Accounts {
Expand Down
Loading