fix(dynatrace): send the only unmute reason the API accepts and request the detail fields the tools map (#6463)

Unmute forwarded the shared muteReason dropdown's FALSE_POSITIVE default, but
Dynatrace accepts exactly one unmute reason, AFFECTED. The tool-level fallback
never fired because a truthy invalid reason was already supplied, so unmute
failed from the block unless the reason was changed by hand.

The vulnerability, problem, and attack detail endpoints omit every optional
property unless it is named in `fields`, so the descriptions, remediation
guidance, affected entities, root-cause evidence, and attacker details those
tools map were always null.
This commit is contained in:
Waleed
2026-08-08 17:10:58 -07:00
committed by GitHub
parent 76b535f676
commit e6485f522d
25 changed files with 284 additions and 60 deletions
@@ -123,7 +123,7 @@ Get the full details of a single Dynatrace problem, including root cause, affect
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the problems.read scope |
| `problemId` | string | Yes | ID of the problem \(e.g., -1234567890123456789_1700000000000V2\) |
| `fields` | string | No | Comma-separated optional properties to include: evidenceDetails, impactAnalysis, recentComments |
| `fields` | string | No | Comma-separated optional properties to include. Defaults to all of them: evidenceDetails, impactAnalysis, recentComments |
#### Output
@@ -581,7 +581,7 @@ Get a single vulnerability with its description, remediation guidance, affected
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the securityProblems.read scope |
| `securityProblemId` | string | Yes | ID of the security problem |
| `fields` | string | No | Comma-separated optional properties to include: +riskAssessment, +managementZones, +codeLevelVulnerabilityDetails, +globalCounts |
| `fields` | string | No | Comma-separated optional properties to include, each prefixed with +. Defaults to every detail property: +riskAssessment, +managementZones, +codeLevelVulnerabilityDetails, +globalCounts, +filteredCounts, +description, +remediationDescription, +events, +vulnerableComponents, +affectedEntities, +exposedEntities, +reachableDataAssets, +relatedEntities, +relatedContainerImages, +relatedAttacks, +entryPoints |
| `managementZoneFilter` | string | No | Restrict the counts to management zones, e.g. names\("Production"\) |
| `from` | string | No | Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-24h. Defaults to the last 24 hours |
@@ -772,7 +772,7 @@ Get a single attack with its entry point, payload, attacker, and the vulnerabili
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the attacks.read scope |
| `attackId` | string | Yes | ID of the attack |
| `fields` | string | No | Comma-separated optional properties to include: +attackTarget, +request, +entrypoint, +vulnerability, +securityProblem, +attacker, +managementZones |
| `fields` | string | No | Comma-separated optional properties to include, each prefixed with +. Defaults to all of them: +attackTarget, +request, +entrypoint, +vulnerability, +securityProblem, +attacker, +managementZones |
#### Output
+46 -7
View File
@@ -90,6 +90,16 @@ const MUTE_OPERATIONS = [
'dynatrace_unmute_security_problems',
]
/**
* Operations that mute. Unmuting is deliberately excluded: Dynatrace accepts
* exactly one unmute reason (`AFFECTED`), so the block sends it itself rather
* than offering a choice that would only ever be wrong.
*/
const MUTE_ONLY_OPERATIONS = ['dynatrace_mute_security_problem', 'dynatrace_mute_security_problems']
/** The only `reason` the Dynatrace unmute endpoints accept. */
const UNMUTE_REASON = 'AFFECTED'
/** Operations that take the full SLO definition. */
const SLO_WRITE_OPERATIONS = ['dynatrace_create_slo', 'dynatrace_update_slo']
@@ -826,10 +836,9 @@ Return ONLY the selector string - no explanations, no surrounding quotes.`,
{ label: 'Vulnerable code not in use', id: 'VULNERABLE_CODE_NOT_IN_USE' },
{ label: 'Ignore', id: 'IGNORE' },
{ label: 'Other', id: 'OTHER' },
{ label: 'Affected (unmute only)', id: 'AFFECTED' },
],
required: true,
condition: { field: 'operation', value: MUTE_OPERATIONS },
condition: { field: 'operation', value: MUTE_ONLY_OPERATIONS },
value: () => 'FALSE_POSITIVE',
},
{
@@ -1398,6 +1407,18 @@ Return ONLY the selector string - no explanations, no surrounding quotes.`,
const toNumber = (value: unknown) =>
value === undefined || value === null || value === '' ? undefined : Number(value)
/**
* Reads a tri-state filter whose "any" position must send no parameter
* at all. The dropdown yields `''`, `'true'`, or `'false'`, but a real
* boolean arrives when the field is wired from an upstream block.
*/
const toOptionalBoolean = (value: unknown) => {
if (typeof value === 'boolean') return value
if (value === 'true') return true
if (value === 'false') return false
return undefined
}
const pagination = {
pageSize: toNumber(params.pageSize),
nextPageKey: params.nextPageKey || undefined,
@@ -1598,7 +1619,6 @@ Return ONLY the selector string - no explanations, no surrounding quotes.`,
}
case 'dynatrace_mute_security_problem':
case 'dynatrace_unmute_security_problem':
return {
...baseParams,
securityProblemId: params.securityProblemId,
@@ -1606,8 +1626,15 @@ Return ONLY the selector string - no explanations, no surrounding quotes.`,
comment: params.muteComment || undefined,
}
case 'dynatrace_unmute_security_problem':
return {
...baseParams,
securityProblemId: params.securityProblemId,
reason: UNMUTE_REASON,
comment: params.muteComment || undefined,
}
case 'dynatrace_mute_security_problems':
case 'dynatrace_unmute_security_problems':
return {
...baseParams,
securityProblemIds: params.securityProblemIds,
@@ -1615,6 +1642,14 @@ Return ONLY the selector string - no explanations, no surrounding quotes.`,
comment: params.muteComment || undefined,
}
case 'dynatrace_unmute_security_problems':
return {
...baseParams,
securityProblemIds: params.securityProblemIds,
reason: UNMUTE_REASON,
comment: params.muteComment || undefined,
}
case 'dynatrace_list_remediation_items':
return {
...baseParams,
@@ -1718,8 +1753,9 @@ Return ONLY the selector string - no explanations, no surrounding quotes.`,
return {
...baseParams,
type: params.monitorType || undefined,
// '' means "any", so send no filter rather than enabled=false.
enabled: params.monitorEnabled ? params.monitorEnabled === 'true' : undefined,
// "Any" must send no filter — enabled=false would return only the
// disabled monitors, the opposite of what was asked for.
enabled: toOptionalBoolean(params.monitorEnabled),
location: params.monitorLocation || undefined,
tag: params.monitorTag || undefined,
managementZone: toNumber(params.monitorManagementZone),
@@ -1859,7 +1895,10 @@ Return ONLY the selector string - no explanations, no surrounding quotes.`,
updateToken: { type: 'string', description: 'Optimistic-concurrency token' },
validateOnly: { type: 'boolean', description: 'Validate without saving' },
monitorType: { type: 'string', description: 'Synthetic monitor type filter' },
monitorEnabled: { type: 'boolean', description: 'Only enabled synthetic monitors' },
monitorEnabled: {
type: 'string',
description: 'Synthetic enabled filter: empty for any, "true", or "false"',
},
monitorLocation: { type: 'string', description: 'Synthetic location filter' },
monitorTag: { type: 'string', description: 'Synthetic monitor tag filter' },
monitorManagementZone: { type: 'number', description: 'Synthetic management zone ID' },
@@ -105,6 +105,14 @@ export const createSettingsObjectTool: ToolConfig<
const entries = Array.isArray(parsed) ? (parsed as Array<Record<string, unknown>>) : []
const results = entries.map(mapSettingsWriteResult)
// A rejected object comes back as 207 with a per-object 4xx, so the HTTP
// status alone would report a failed create as a success.
const failure = results.find((result) => result.code !== null && result.code >= 400)
if (failure) {
const message = (failure.writeError?.message as string) ?? `HTTP ${failure.code}`
throw new Error(`Dynatrace rejected the settings object: ${message}`)
}
return {
success: true,
output: {
+125 -2
View File
@@ -7,10 +7,12 @@ import { addTagsTool } from '@/tools/dynatrace/add_tags'
import { closeProblemTool } from '@/tools/dynatrace/close_problem'
import { createSettingsObjectTool } from '@/tools/dynatrace/create_settings_object'
import { createSloTool } from '@/tools/dynatrace/create_slo'
import { getAttackTool } from '@/tools/dynatrace/get_attack'
import { getAuditLogsTool } from '@/tools/dynatrace/get_audit_logs'
import { getEntityTool } from '@/tools/dynatrace/get_entity'
import { getMetricTool } from '@/tools/dynatrace/get_metric'
import { getProblemTool } from '@/tools/dynatrace/get_problem'
import { getSecurityProblemTool } from '@/tools/dynatrace/get_security_problem'
import { getSloTool } from '@/tools/dynatrace/get_slo'
import { getSyntheticBatchTool } from '@/tools/dynatrace/get_synthetic_batch'
import { ingestEventTool } from '@/tools/dynatrace/ingest_event'
@@ -91,8 +93,8 @@ describe('path identifiers', () => {
const base = { environmentUrl: ENV, apiToken: TOKEN }
it('trims whitespace pasted around an identifier', () => {
expect(url(getProblemTool, { ...base, problemId: ' P-123_456V2 ' })).toBe(
`${ENV}/api/v2/problems/P-123_456V2`
expect(new URL(url(getProblemTool, { ...base, problemId: ' P-123_456V2 ' })).pathname).toBe(
'/api/v2/problems/P-123_456V2'
)
expect(url(getEntityTool, { ...base, entityId: ' HOST-06F288EE2A930951\n' })).toBe(
`${ENV}/api/v2/entities/HOST-06F288EE2A930951`
@@ -148,6 +150,11 @@ describe('new-surface request shaping', () => {
expect(call('true').enabled).toBe(true)
expect(call('false').enabled).toBe(false)
// A value wired from an upstream block arrives as a real boolean, which must
// not read as "disabled only" the way `true === 'true'` would.
expect(call(true as unknown as string).enabled).toBe(true)
expect(call(false as unknown as string).enabled).toBe(false)
expect(url(listSyntheticMonitorsTool, { environmentUrl: ENV, apiToken: TOKEN })).toBe(
`${ENV}/api/v1/synthetic/monitors`
)
@@ -704,4 +711,120 @@ describe('response mapping', () => {
expect(result.output.accepted).toBe(false)
expect(result.output.details).toEqual({ error: { message: 'some invalid' } })
})
it('fails a settings write that Dynatrace rejected per-object under a 2xx', async () => {
const rejected = JSON.stringify([
{ code: 400, error: { code: 400, message: 'value.enabled is required' } },
])
await expect(
createSettingsObjectTool.transformResponse!(new Response(rejected, { status: 207 }))
).rejects.toThrow(/value.enabled is required/)
await expect(
updateSettingsObjectTool.transformResponse!(
new Response(JSON.stringify({ code: 400, error: { message: 'schema mismatch' } }), {
status: 207,
})
)
).rejects.toThrow(/schema mismatch/)
})
})
describe('detail requests ask for the properties they map', () => {
const base = { environmentUrl: ENV, apiToken: TOKEN }
it('requests every optional vulnerability property by default', () => {
// Dynatrace omits description, remediation guidance, and affected entities
// unless they are named in `fields`, so an unset default would map nulls.
const requested = new URL(
url(getSecurityProblemTool, { ...base, securityProblemId: 'S-1' })
).searchParams
.get('fields')
?.split(',')
expect(requested).toEqual(
expect.arrayContaining([
'+description',
'+remediationDescription',
'+affectedEntities',
'+vulnerableComponents',
'+riskAssessment',
])
)
// An explicit choice still wins.
expect(
new URL(
url(getSecurityProblemTool, {
...base,
securityProblemId: 'S-1',
fields: '+riskAssessment',
})
).searchParams.get('fields')
).toBe('+riskAssessment')
})
it('requests every optional attack property by default', () => {
expect(
new URL(url(getAttackTool, { ...base, attackId: 'A-1' })).searchParams.get('fields')
).toBe(
'+attackTarget,+request,+entrypoint,+vulnerability,+securityProblem,+attacker,+managementZones'
)
expect(
new URL(
url(getAttackTool, { ...base, attackId: 'A-1', fields: '+attacker' })
).searchParams.get('fields')
).toBe('+attacker')
})
it('requests the optional problem properties by default', () => {
expect(
new URL(url(getProblemTool, { ...base, problemId: 'P-1' })).searchParams.get('fields')
).toBe('evidenceDetails,impactAnalysis,recentComments')
expect(
new URL(
url(getProblemTool, { ...base, problemId: 'P-1', fields: 'impactAnalysis' })
).searchParams.get('fields')
).toBe('impactAnalysis')
})
})
describe('mute state writes', () => {
const params = (DynatraceBlock.tools.config?.params ?? (() => ({}))) as (
p: Record<string, unknown>
) => Record<string, unknown>
const call = (operation: string) =>
params({
operation,
environmentUrl: ENV,
apiToken: TOKEN,
securityProblemId: 'S-1',
securityProblemIds: 'S-1, S-2',
// The shared dropdown's default. It is a valid mute reason and an invalid
// unmute one, so forwarding it would make every unmute fail.
muteReason: 'FALSE_POSITIVE',
})
it('sends AFFECTED for an unmute, the only reason the API accepts', () => {
expect(call('dynatrace_unmute_security_problem').reason).toBe('AFFECTED')
expect(call('dynatrace_unmute_security_problems').reason).toBe('AFFECTED')
})
it('still forwards the chosen reason for a mute', () => {
expect(call('dynatrace_mute_security_problem').reason).toBe('FALSE_POSITIVE')
expect(call('dynatrace_mute_security_problems').reason).toBe('FALSE_POSITIVE')
})
it('offers only mute reasons in the dropdown, and only to the mute operations', () => {
const reason = DynatraceBlock.subBlocks.find((sb) => sb.id === 'muteReason')
expect(reason?.options).not.toContainEqual(expect.objectContaining({ id: 'AFFECTED' }))
expect(reason?.condition).toEqual({
field: 'operation',
value: ['dynatrace_mute_security_problem', 'dynatrace_mute_security_problems'],
})
})
})
+17 -2
View File
@@ -10,6 +10,21 @@ import {
import { ErrorExtractorId } from '@/tools/error-extractors'
import type { ToolConfig } from '@/tools/types'
/**
* Every optional property of the attack detail endpoint. Dynatrace omits them
* unless they are requested, so without this default the entry point, payload,
* attacker, and exploited vulnerability the tool maps would all be null.
*/
const ATTACK_DETAIL_FIELDS = [
'+attackTarget',
'+request',
'+entrypoint',
'+vulnerability',
'+securityProblem',
'+attacker',
'+managementZones',
].join(',')
export const getAttackTool: ToolConfig<DynatraceGetAttackParams, DynatraceGetAttackResponse> = {
id: 'dynatrace_get_attack',
name: 'Dynatrace Get Attack',
@@ -43,14 +58,14 @@ export const getAttackTool: ToolConfig<DynatraceGetAttackParams, DynatraceGetAtt
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated optional properties to include: +attackTarget, +request, +entrypoint, +vulnerability, +securityProblem, +attacker, +managementZones',
'Comma-separated optional properties to include, each prefixed with +. Defaults to all of them: +attackTarget, +request, +entrypoint, +vulnerability, +securityProblem, +attacker, +managementZones',
},
},
request: {
url: (params) =>
buildDynatraceUrl(params.environmentUrl, `/attacks/${encodeDynatraceId(params.attackId)}`, {
fields: params.fields,
fields: params.fields || ATTACK_DETAIL_FIELDS,
}),
method: 'GET',
headers: (params) => dynatraceHeaders(params.apiToken),
+3 -3
View File
@@ -60,19 +60,19 @@ export const getAuditLogsTool: ToolConfig<
sort: {
type: 'string',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'timestamp for oldest first, or -timestamp for newest first (default)',
},
pageSize: {
type: 'number',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Entries per page (max 5000, default 1000)',
},
nextPageKey: {
type: 'string',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Cursor for the next page. All other filters are ignored when it is set',
},
},
+9 -2
View File
@@ -13,6 +13,13 @@ import {
import { ErrorExtractorId } from '@/tools/error-extractors'
import type { ToolConfig } from '@/tools/types'
/**
* Every optional property of the problem detail endpoint. Dynatrace omits them
* unless they are requested, so the root cause evidence and impact analysis the
* tool maps would otherwise always be null.
*/
const PROBLEM_DETAIL_FIELDS = 'evidenceDetails,impactAnalysis,recentComments'
export const getProblemTool: ToolConfig<DynatraceGetProblemParams, DynatraceGetProblemResponse> = {
id: 'dynatrace_get_problem',
name: 'Dynatrace Get Problem',
@@ -46,14 +53,14 @@ export const getProblemTool: ToolConfig<DynatraceGetProblemParams, DynatraceGetP
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated optional properties to include: evidenceDetails, impactAnalysis, recentComments',
'Comma-separated optional properties to include. Defaults to all of them: evidenceDetails, impactAnalysis, recentComments',
},
},
request: {
url: (params) =>
buildDynatraceUrl(params.environmentUrl, `/problems/${encodeDynatraceId(params.problemId)}`, {
fields: params.fields,
fields: params.fields || PROBLEM_DETAIL_FIELDS,
}),
method: 'GET',
headers: (params) => dynatraceHeaders(params.apiToken),
@@ -13,6 +13,31 @@ import {
import { ErrorExtractorId } from '@/tools/error-extractors'
import type { ToolConfig } from '@/tools/types'
/**
* Every optional property of the vulnerability detail endpoint. Dynatrace omits
* all of them unless they are requested, so without this default the tool would
* answer with nulls for the description, remediation guidance, and affected
* entities it exists to return.
*/
const SECURITY_PROBLEM_DETAIL_FIELDS = [
'+riskAssessment',
'+managementZones',
'+codeLevelVulnerabilityDetails',
'+globalCounts',
'+filteredCounts',
'+description',
'+remediationDescription',
'+events',
'+vulnerableComponents',
'+affectedEntities',
'+exposedEntities',
'+reachableDataAssets',
'+relatedEntities',
'+relatedContainerImages',
'+relatedAttacks',
'+entryPoints',
].join(',')
export const getSecurityProblemTool: ToolConfig<
DynatraceGetSecurityProblemParams,
DynatraceGetSecurityProblemResponse
@@ -49,7 +74,7 @@ export const getSecurityProblemTool: ToolConfig<
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated optional properties to include: +riskAssessment, +managementZones, +codeLevelVulnerabilityDetails, +globalCounts',
'Comma-separated optional properties to include, each prefixed with +. Defaults to every detail property: +riskAssessment, +managementZones, +codeLevelVulnerabilityDetails, +globalCounts, +filteredCounts, +description, +remediationDescription, +events, +vulnerableComponents, +affectedEntities, +exposedEntities, +reachableDataAssets, +relatedEntities, +relatedContainerImages, +relatedAttacks, +entryPoints',
},
managementZoneFilter: {
type: 'string',
@@ -72,7 +97,7 @@ export const getSecurityProblemTool: ToolConfig<
params.environmentUrl,
`/securityProblems/${encodeDynatraceId(params.securityProblemId)}`,
{
fields: params.fields,
fields: params.fields || SECURITY_PROBLEM_DETAIL_FIELDS,
managementZoneFilter: params.managementZoneFilter,
from: params.from,
}
+1 -1
View File
@@ -54,7 +54,7 @@ export const getSloTool: ToolConfig<DynatraceGetSloParams, DynatraceGetSloRespon
timeFrame: {
type: 'string',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description:
'CURRENT evaluates the SLO over its own timeframe; GTF evaluates over the From/To range',
},
+3 -3
View File
@@ -70,20 +70,20 @@ export const listAttacksTool: ToolConfig<DynatraceListAttacksParams, DynatraceLi
sort: {
type: 'string',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description:
'Sort by displayId, displayName, attackType, state, sourceIp, requestPath, or timestamp with a + or - prefix',
},
pageSize: {
type: 'number',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Attacks per page (max 500, default 100)',
},
nextPageKey: {
type: 'string',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Cursor for the next page. All other filters are ignored when it is set',
},
},
+3 -3
View File
@@ -72,19 +72,19 @@ export const listEntitiesTool: ToolConfig<
sort: {
type: 'string',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Sort by display name: +displayName ascending or -displayName descending',
},
pageSize: {
type: 'number',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Entities per page (default 50)',
},
nextPageKey: {
type: 'string',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Cursor for the next page. All other filters are ignored when it is set',
},
},
@@ -40,13 +40,13 @@ export const listEntityTypesTool: ToolConfig<
pageSize: {
type: 'number',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Entity types per page (max 500, default 50)',
},
nextPageKey: {
type: 'string',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Cursor for the next page. Page size is ignored when it is set',
},
},
+2 -2
View File
@@ -70,13 +70,13 @@ export const listEventsTool: ToolConfig<DynatraceListEventsParams, DynatraceList
pageSize: {
type: 'number',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Events per page (max 1000, default 100)',
},
nextPageKey: {
type: 'string',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Cursor for the next page. All other filters are ignored when it is set',
},
},
+3 -3
View File
@@ -70,7 +70,7 @@ export const listMetricsTool: ToolConfig<DynatraceListMetricsParams, DynatraceLi
writtenSinceMode: {
type: 'string',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description:
'INCLUDE (default) keeps metrics written since Written Since; EXCLUDE keeps the ones not written since then',
},
@@ -83,13 +83,13 @@ export const listMetricsTool: ToolConfig<DynatraceListMetricsParams, DynatraceLi
pageSize: {
type: 'number',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Metrics per page (max 500, default 100)',
},
nextPageKey: {
type: 'string',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Cursor for the next page. All other filters are ignored when it is set',
},
},
@@ -52,13 +52,13 @@ export const listProblemCommentsTool: ToolConfig<
pageSize: {
type: 'number',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Comments per page (max 500, default 10)',
},
nextPageKey: {
type: 'string',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Cursor for the next page. Page size is ignored when it is set',
},
},
+2 -2
View File
@@ -87,13 +87,13 @@ export const listProblemsTool: ToolConfig<
pageSize: {
type: 'number',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Problems per page (max 500, default 50)',
},
nextPageKey: {
type: 'string',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Cursor for the next page. All other filters are ignored when it is set',
},
},
@@ -72,19 +72,19 @@ export const listSecurityProblemsTool: ToolConfig<
sort: {
type: 'string',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Sort by a field with a + or - prefix, e.g. -riskAssessment.riskScore',
},
pageSize: {
type: 'number',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Security problems per page (max 500, default 100)',
},
nextPageKey: {
type: 'string',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Cursor for the next page. All other filters are ignored when it is set',
},
},
@@ -76,19 +76,19 @@ export const listSettingsObjectsTool: ToolConfig<
sort: {
type: 'string',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Sort expression, e.g. -modified',
},
pageSize: {
type: 'number',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Objects per page (max 500, default 100)',
},
nextPageKey: {
type: 'string',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Cursor for the next page. All other filters are ignored when it is set',
},
},
+7 -7
View File
@@ -54,44 +54,44 @@ export const listSlosTool: ToolConfig<DynatraceListSlosParams, DynatraceListSlos
timeFrame: {
type: 'string',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description:
'CURRENT evaluates each SLO over its own timeframe; GTF evaluates over the From/To range',
},
sort: {
type: 'string',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Sort by name: name ascending or -name descending',
},
enabledSlos: {
type: 'string',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Filter by enabled state: true, false, or all',
},
evaluate: {
type: 'boolean',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Evaluate each SLO and include its calculated values',
},
showGlobalSlos: {
type: 'boolean',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Include SLOs that are not scoped to a management zone',
},
pageSize: {
type: 'number',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'SLOs per page (max 10000, default 10)',
},
nextPageKey: {
type: 'string',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Cursor for the next page. All other filters are ignored when it is set',
},
},
+1 -3
View File
@@ -1,4 +1,4 @@
import { nextPageKeyOutput, totalCountOutput, warningsOutput } from '@/tools/dynatrace/outputs'
import { totalCountOutput, warningsOutput } from '@/tools/dynatrace/outputs'
import type {
DynatraceQueryMetricsParams,
DynatraceQueryMetricsResponse,
@@ -103,7 +103,6 @@ export const queryMetricsTool: ToolConfig<
result: result.map(mapMetricResult),
resolution: (data.resolution as string) ?? null,
totalCount: (data.totalCount as number) ?? null,
nextPageKey: (data.nextPageKey as string) ?? null,
warnings: mapWarnings(data.warnings),
},
}
@@ -183,7 +182,6 @@ export const queryMetricsTool: ToolConfig<
nullable: true,
},
totalCount: totalCountOutput,
nextPageKey: nextPageKeyOutput,
warnings: warningsOutput,
},
}
+1 -1
View File
@@ -69,7 +69,7 @@ export const searchLogsTool: ToolConfig<DynatraceSearchLogsParams, DynatraceSear
nextSliceKey: {
type: 'string',
required: false,
visibility: 'user-only',
visibility: 'user-or-llm',
description: 'Cursor for the next slice. All other filters are ignored when it is set',
},
},
-1
View File
@@ -341,7 +341,6 @@ export interface DynatraceQueryMetricsResponse extends ToolResponse {
result: DynatraceMetricResult[]
resolution: string | null
totalCount: number | null
nextPageKey: string | null
warnings: string[]
}
}
@@ -92,12 +92,22 @@ export const updateSettingsObjectTool: ToolConfig<
transformResponse: async (response: Response, params?: DynatraceUpdateSettingsObjectParams) => {
const data = await readJsonBody(response)
const code = (data.code as number) ?? response.status
// The endpoint answers 207 with a per-object status, so a rejected update
// would otherwise be reported as a success.
if (code >= 400) {
const error = data.error as Record<string, unknown> | undefined
throw new Error(
`Dynatrace rejected the settings object: ${(error?.message as string) ?? `HTTP ${code}`}`
)
}
return {
success: true,
output: {
objectId: (data.objectId as string) ?? params?.objectId ?? null,
code: (data.code as number) ?? response.status,
code,
},
}
},
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long