Skip to content

feat: add Dynatrace integration - #282

Merged
kolatts merged 2 commits into
mainfrom
kolatts/281-dynatrace-integration
Jul 29, 2026
Merged

feat: add Dynatrace integration#282
kolatts merged 2 commits into
mainfrom
kolatts/281-dynatrace-integration

Conversation

@kolatts

@kolatts kolatts commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

  • add Dynatrace Environment API support for monitored entities, services, Kubernetes workloads, and problems
  • add distributed-trace retrieval through the supported Grail DQL Query API with separate platform authentication
  • add environment/config resolution, secret masking, connectivity checks, interactive setup, CLI help, and pncli skill documentation
  • allow the Claude triage GitHub App token to request workflows: write for explicitly scoped workflow/CI issues

Why

The original automated implementation for #281 was committed only in an ephemeral Actions checkout and could not be pushed. Its proposed trace handling also relied on traces.lookup, which checks trace presence for cross-environment tracing but does not retrieve trace spans. This implementation rebuilds the integration against the supported Dynatrace Environment and Platform APIs.

User impact

Users can configure a classic Dynatrace API token for entities/problems and, optionally, a platform token for Grail-backed trace queries. All commands return pncli's structured JSON output and require no external Dynatrace CLI.

Validation

  • npm run typecheck
  • npm run lint
  • npm test (396 tests)
  • npm run build
  • CLI help smoke test

Closes #281

Add supported Environment API operations and Grail-backed distributed trace queries. Allow the Claude triage app token to request workflows:write for explicitly scoped CI changes.

Closes #281

Generated-with: OpenAI Codex (GPT-5)
@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kolatts's task in 5m 2s —— View job


PR Review: feat: add Dynatrace integration

  • Gather context and read previous Claude comments
  • Review changed files
    • src/types/config.ts
    • src/lib/config.ts + config.test.ts
    • src/lib/http.ts + http.test.ts
    • src/services/dynatrace/commands.ts
    • src/services/config/commands.ts
    • src/cli.ts
    • skills/pncli/dynatrace.md + SKILL.md
    • .github/workflows/claude-triage.yml
  • Post inline comments on 3 findings
  • Submit formal PR review (approved)

Approved. Clean, complete integration that follows established patterns.

What's solid:

  • All 7 required CLAUDE.md files updated together — nothing missing
  • DQL injection correctly blocked: the /^[0-9a-f]{16,32}$/i regex ensures the trace ID is hex-only before interpolation
  • Correct auth scheme: Api-Token for the classic Environment API, Bearer for the Grail platform — tests verify both
  • allPages pagination follows Dynatrace's cursor model correctly
  • Env vars have highest precedence for all four Dynatrace fields
  • Config wizard cleanly separates classic vs. platform setup and verifies classic connectivity before saving

Minor findings (inline comments posted):

  1. commands.ts:90 — poll-loop attempt counter works, but a wall-clock deadline would be more robust if Dynatrace doesn't honour request-timeout. Suggestion only.
  2. config/commands.ts:268 — platform credentials aren't exercised by config test / config check. Easy follow-up.
  3. claude-triage.yml:167workflows: write is a notable permission for an agent processing untrusted input; mitigated by existing prompt guardrails but worth awareness.

Comment thread src/services/dynatrace/commands.ts Outdated

for (let attempt = 0; response.state === 'RUNNING' || response.state === 'NOT_STARTED'; attempt++) {
if (!response.requestToken) throw new PncliError('Dynatrace did not return a query request token');
if (attempt >= 11) throw new PncliError('Dynatrace trace query did not finish within 60 seconds');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion — poll-loop timing claim is approximately right, but fragile if long-polling isn't honoured

The attempt >= 11 guard allows 11 poll calls (attempts 0–10). Each poll sends request-timeout=5000, which instructs Dynatrace to hold the connection for up to 5 s before responding. So worst-case wall time is ≈ 55 s (11 × 5 s) plus the initial execute call — close enough to the "60 seconds" claim.

The concern: if Dynatrace returns a RUNNING response immediately (e.g. the long-poll param isn't honoured or the server is under load), all 11 attempts burn in milliseconds, and the user sees the timeout error after < 1 s, which is confusing. Consider adding a small floor sleep when the response comes back faster than expected, or using a wall-clock deadline instead of an attempt counter:

const deadline = Date.now() + 60_000;
while (response.state === 'RUNNING' || response.state === 'NOT_STARTED') {
  if (!response.requestToken) throw new PncliError('Dynatrace did not return a query request token');
  if (Date.now() >= deadline) throw new PncliError('Dynatrace trace query did not finish within 60 seconds');
  // ...poll...
}

This is a suggestion, not a blocker — the current approach works correctly when Dynatrace honours the long-poll timeout.

results.openshift = { ok: null, message: 'not configured' };
}

if (cfg.dynatrace.baseUrl && cfg.dynatrace.apiToken) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit — config test doesn't exercise the platform token

The connectivity test fires a dynatrace (classic Environment API) call, but there's no check for platformUrl / platformToken. If the platform credentials are misconfigured the user won't know until they hit a runtime error in dynatrace trace. The same gap exists in config check (line ~530).

It's fine for a first pass — adding dynatrace_platform as a separate result key when platformUrl && platformToken are set would close the gap. Not blocking.

claude_args: |
--model sonnet --max-turns 300 --allowedTools "Read,Glob,Grep,Edit,Write,Bash(gh issue list:*),Bash(gh issue comment:*),Bash(gh issue close:*),Bash(gh issue edit:*),Bash(gh label create:*),Bash(gh pr view:*),Bash(npm run build:*),Bash(npm run typecheck:*),Bash(npm run lint:*),Bash(npm test:*),Bash(git checkout -b:*),Bash(git add:*),Bash(git commit:*),Bash(git push:*)"
additional_permissions: |
workflows: write

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security note — workflows: write is a meaningful permission escalation for an automated agent processing untrusted input

The triage agent now has the ability to modify .github/workflows/ files. The prompt already includes strong anti-injection guardrails (explicit untrusted-input framing, allow-list for when workflow edits are permitted, security assessment gate), so the risk is mitigated in practice.

Worth keeping in mind: this is the permission most valuable to an adversary trying to exploit a prompt-injection attack via a crafted issue body. If you see an issue that requests a workflow change and whose body contains anything suspicious, double-check the resulting commit carefully before merging. The existing "modify workflow or CI files only when the issue explicitly requests..." guardrail in the prompt is the main control here.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — Approve

Clean, complete integration that follows the established patterns throughout.

What's solid

Architecture: All seven required files from CLAUDE.md updated together — types, config resolution, http client, config commands, CLI registration, skill doc, skill index. Nothing missing.

Security: DQL injection in the trace query is correctly neutralised. The regex /^[0-9a-f]{16,32}$/i validates the trace ID to hex-only before it's interpolated into the DQL string, so there's no injection surface. Credential masking covers both apiToken and platformToken.

Auth: Correct token scheme for each endpoint — Api-Token for the classic Environment API, Bearer for the Grail platform API. Tests verify both paths.

Pagination: The allPages / do-while loop matches Dynatrace's cursor-based pagination. totalCount returned is the actual fetched count rather than the API's pre-pagination estimate, which is the right call since we always exhaust all pages.

Config resolution: Env vars have highest precedence for all four Dynatrace fields, consistent with the project's global precedence rule.

CLI UX: The wizard splits classic vs. platform config cleanly, verifies the classic connection before saving, and gives correct setup instructions. config check and config test treat Dynatrace consistently with other services.

Minor findings (see inline comments)

  • Poll loop (commands.ts:90): attempt-counter works correctly with Dynatrace long-polling, but a wall-clock deadline would be more robust if the server returns RUNNING without honouring request-timeout. Suggestion only.
  • Platform creds not verified (config/commands.ts:268): config test / config check only exercise the classic API; misconfigured platform token goes undetected until dynatrace trace is called. Easy follow-up, not a blocker.
  • workflows: write permission (claude-triage.yml:167): noted the tradeoff — mitigated by strong prompt guardrails, but worth awareness.

@kolatts
kolatts marked this pull request as ready for review July 29, 2026 19:44
@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Claude Code is working…

  • Clean up stale Claude comments and reviews
  • Read changed files
  • Review for correctness, security, UX, and pattern adherence
  • Post inline comments on findings
  • Submit formal review

View job run

Use a wall-clock trace polling deadline and validate optional Grail platform credentials during config checks.

Generated-with: OpenAI Codex (GPT-5)
@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kolatts's task in 6m 25s —— View job


PR Review: feat: add Dynatrace integration

  • Clean up stale Claude comments and reviews
  • Read changed files (commands.ts, http.ts, config.ts, types/config.ts, config/commands.ts, http.test.ts, config.test.ts, dynatrace.md, claude-triage.yml)
  • Review for correctness, security, UX, and pattern adherence
  • Post inline comments on findings
  • Submit formal review (approved)

Approved. Both substantive findings from the previous review pass have been addressed in this commit.

What changed since the first review

Poll-loop timing — the wall-clock deadline suggestion is now implemented. runTraceQuery uses Date.now() + 60_000 as a hard deadline and adds a 1-second floor sleep when the server returns RUNNING without honouring the long-poll timeout. Both behaviours are correct.

Platform credential coverageconfig test (lines 281–299) and config check (lines 566–587) now both exercise the platform token separately and report as dynatrace_platform. The partial-config case (one field set, not both) is caught and reported as an error in both commands.

New finding (nit, inline comments posted)

labelWidth = 14 in the config check table (commands.ts:597 and :615) is too narrow for dynatrace_platform (18 chars). The padEnd(14) call is a no-op on a string already longer than 14, shifting that row's status column 4 characters to the right. Bumping to 20 fixes the alignment.

Rest of the integration is solid

  • All seven CLAUDE.md-required files updated
  • Api-Token / Bearer scheme correct per endpoint type; tests verify both
  • Hex-only trace ID validation before DQL interpolation — no injection surface
  • encodeURIComponent on entity/problem IDs in URL path segments
  • Env vars have highest precedence for all four Dynatrace fields
  • Both tokens masked in maskConfig

@kolatts
kolatts merged commit db99b92 into main Jul 29, 2026
2 checks passed
@kolatts
kolatts deleted the kolatts/281-dynatrace-integration branch July 29, 2026 19:47
// Human-readable table to stdout
const services = ['jira', 'bitbucket', 'github', 'confluence', 'sonar', 'sde', 'ado', 'jenkins', 'udeploy', 'artifactory', 'checkmarx', 'servicenow', 'contrast', 'sonatypeiq', 'openshift'] as const;
const services = ['jira', 'bitbucket', 'github', 'confluence', 'sonar', 'sde', 'ado', 'jenkins', 'udeploy', 'artifactory', 'checkmarx', 'servicenow', 'contrast', 'sonatypeiq', 'openshift', 'dynatrace', 'dynatrace_platform'] as const;
const labelWidth = 14;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit — labelWidth = 14 is too narrow for dynatrace_platform

dynatrace_platform is 18 characters. svc.padEnd(14) on a string already longer than 14 is a no-op, so the status column for that row will be shifted 4 characters to the right compared to every other service, misaligning the table.

Suggested change
const labelWidth = 14;
const labelWidth = 20;

Same change needed on line 615 (the --pretty branch).

if (opts.pretty) {
const services = ['jira', 'bitbucket', 'github', 'confluence', 'sonar', 'sde', 'ado', 'jenkins', 'udeploy', 'artifactory', 'checkmarx', 'servicenow', 'contrast', 'sonatypeiq', 'openshift'] as const;
const services = ['jira', 'bitbucket', 'github', 'confluence', 'sonar', 'sde', 'ado', 'jenkins', 'udeploy', 'artifactory', 'checkmarx', 'servicenow', 'contrast', 'sonatypeiq', 'openshift', 'dynatrace', 'dynatrace_platform'] as const;
const labelWidth = 14;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same labelWidth issue as line 597 — needs to be 20 here too to align dynatrace_platform.

Suggested change
const labelWidth = 14;
const labelWidth = 20;

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — Approve

Clean follow-up commit that addresses both substantive findings from the first review pass.

What changed since the last review

Poll-loop robustness (previously: suggestion) — now implemented correctly. commands.ts uses a Date.now() + 60_000 wall-clock deadline instead of an attempt counter, and includes a 1-second floor sleep when the server returns a RUNNING response faster than expected. Both the deadline guard and the fast-response floor were added; the logic is correct.

Platform credential coverage (previously: nit) — both config test and config check now exercise the platform token separately, reporting results as dynatrace_platform. The half-configured case (one of the two platform fields set, not both) is correctly flagged as an error in both commands.

New finding (nit — inline comments posted)

labelWidth = 14 in the config check table renderer (commands.ts:597 and :615) is too narrow for dynatrace_platform (18 chars). padEnd(14) is a no-op on strings already longer than 14, so that row's status column shifts 4 characters right. Bump to 20 to keep the table aligned.

Rest of the integration holds up

  • All seven required CLAUDE.md files updated
  • Auth scheme correct: Api-Token for classic Environment API, Bearer for Grail platform
  • Hex-only validation of trace ID before DQL interpolation — no injection surface
  • encodeURIComponent on entity/problem IDs in URL path segments
  • Env vars have highest precedence for all four Dynatrace fields
  • Both tokens masked in maskConfig
  • Tests cover both auth paths and env-var resolution

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dynatrace integration

1 participant