Skip to content

feat: harden operations and automate test releases - #233

Merged
CrimsonSoul merged 13 commits into
testfrom
codex/dynatrace-problems-hardening
Aug 12, 2026
Merged

feat: harden operations and automate test releases#233
CrimsonSoul merged 13 commits into
testfrom
codex/dynatrace-problems-hardening

Conversation

@CrimsonSoul

@CrimsonSoul CrimsonSoul commented Aug 12, 2026

Copy link
Copy Markdown
Owner

What changed

  • Adds Dynatrace Problems operational hardening, demo data, and Cloud Status integration.
  • Unifies Dynatrace and Juniper Mist provider rows in the outage-focused Service Status view.
  • Automates normal GitHub releases from protected test updates using Conventional Commits.
  • Packages the exact tested SHA as a versioned Windows executable with a SHA-256 checksum.
  • Keeps manual screenshot generation out of normal background Electron regression runs.

Release behavior

  • Waits for Build quality, SonarQube, and Snyk gates on the exact test SHA.
  • Creates the first release as v1.0.0, then derives major/minor/patch versions from commit history.
  • Reuses verified releases on rerun and rebuilds incomplete release assets.

Verification

  • npm run typecheck
  • npm run lint
  • npm run format:check
  • npm test (5,907 passing, 1 existing skip)
  • npm run build
  • npm audit --audit-level=high --omit=dev (0 vulnerabilities)
  • npm run test:electron (38 passing, 1 intentional screenshot skip; three timing-only failures each passed targeted rerun)
  • git diff --check

Summary by CodeRabbit

  • New Features
    • Added Dynatrace to Service Status with incident severity, affected scopes, deduplication, and outage details.
    • Improved Dynatrace Problems with paginated history, retry and truncation notices, impacted entities, timestamps, and Service Desk actions.
    • Added faster, more resilient offline browsing with pagination and cached query results.
    • Added secure, validated Service Desk link opening.
  • Bug Fixes
    • Retained out-of-scope Dynatrace problems safely instead of removing them.
  • Documentation
    • Added Windows download guidance and automated release documentation.

@CrimsonSoul
CrimsonSoul enabled auto-merge August 12, 2026 09:36
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7cf10cf0-bbd3-4943-b219-b167cc754892

📥 Commits

Reviewing files that changed from the base of the PR and between 08227bd and 931e34f.

📒 Files selected for processing (13)
  • docs/SECURITY.md
  • scripts/release-version.mjs
  • scripts/release-version.test.mjs
  • src/main/handlers/cacheHandlers.ts
  • src/renderer/src/components/settings/AdministrationSettings.test.tsx
  • src/renderer/src/components/settings/administration/RelayServerPanel.tsx
  • src/renderer/src/hooks/__tests__/useCollection.test.ts
  • src/renderer/src/stores/collectionStore.ts
  • src/renderer/src/tabs/DynatraceProblemsTab.tsx
  • src/renderer/src/tabs/__tests__/DynatraceProblemsTab.test.tsx
  • src/renderer/src/tabs/dynatrace-problems.css
  • src/shared/cloudStatus.ts
  • src/shared/urlSecurity.ts
🚧 Files skipped from review as they are similar to previous changes (13)
  • docs/SECURITY.md
  • src/shared/urlSecurity.ts
  • src/main/handlers/cacheHandlers.ts
  • scripts/release-version.test.mjs
  • src/renderer/src/components/settings/AdministrationSettings.test.tsx
  • src/renderer/src/components/settings/administration/RelayServerPanel.tsx
  • src/shared/cloudStatus.ts
  • src/renderer/src/hooks/tests/useCollection.test.ts
  • src/renderer/src/tabs/dynatrace-problems.css
  • scripts/release-version.mjs
  • src/renderer/src/tabs/tests/DynatraceProblemsTab.test.tsx
  • src/renderer/src/stores/collectionStore.ts
  • src/renderer/src/tabs/DynatraceProblemsTab.tsx

📝 Walkthrough
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main themes: operational hardening and automated releases from the test branch.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/dynatrace-problems-hardening

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (19)
.github/workflows/release.yml (1)

201-222: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add pipefail and clean the temporary directory.

The step uses set -u only. The sha256sum --check result is evaluated by the if, so the missing -e is intentional. Adding -o pipefail still protects future pipelines in this step. Removing verify_dir avoids leaving downloaded assets on the runner.

♻️ Proposed cleanup
-          set -u
+          set -uo pipefail
           verify_dir="$(mktemp -d)"
+          trap 'rm -rf "$verify_dir"' EXIT
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 201 - 222, Update the “Verify
existing release assets” step to enable Bash pipefail alongside nounset without
adding errexit, preserving the current conditional verification flow. Add
cleanup for the temporary verify_dir so downloaded assets are removed after the
verification attempt, including when the step completes through either branch.
scripts/release-version.test.mjs (2)

162-177: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Isolate the fixture repository from global Git configuration.

The test runs git commit with the developer's global configuration. If a contributor sets commit.gpgsign=true, init.templateDir, or a global hooks path, the commit fails or waits for a passphrase. Setting GIT_CONFIG_GLOBAL and GIT_CONFIG_SYSTEM to a path that does not exist makes the fixture deterministic.

♻️ Proposed isolation
-function git(cwd, ...args) {
+const isolatedGitEnv = {
+  ...process.env,
+  GIT_CONFIG_GLOBAL: '/dev/null',
+  GIT_CONFIG_SYSTEM: '/dev/null',
+};
+
+function git(cwd, ...args) {
   // Git is the fixture engine for this isolated temporary repository.
   // eslint-disable-next-line sonarjs/no-os-command-from-path
-  return execFileSync('git', args, { cwd, encoding: 'utf8' }).trim();
+  return execFileSync('git', args, { cwd, encoding: 'utf8', env: isolatedGitEnv }).trim();
 }

Add -c, commit.gpgsign=false to the commit invocation if you prefer to keep the ambient environment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/release-version.test.mjs` around lines 162 - 177, Isolate the
temporary Git repository from system and global configuration in the test around
the git commit setup. Set GIT_CONFIG_GLOBAL and GIT_CONFIG_SYSTEM to nonexistent
paths in the spawned Git environment, or disable signing directly on the commit
invocation with commit.gpgsign=false, so global templates, hooks, signing, and
passphrase prompts cannot affect the fixture.

179-202: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add narrow tests for the rejection paths.

The suite covers the success path of writeGitHubOutputs only through the integration test. assertOutputResult and the headSha guard in planRelease protect the release outputs, and neither guard has a direct test. Add small unit tests that assert planRelease throws for a short or uppercase headSha, and that writeGitHubOutputs throws for a malformed action and for an empty outputPath.

This follows the guideline "Start with the narrowest tests that cover the changed behavior."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/release-version.test.mjs` around lines 179 - 202, Add focused unit
tests for the rejection paths in planRelease and writeGitHubOutputs: verify
planRelease throws for short and uppercase headSha values, and verify
writeGitHubOutputs throws when action is malformed or outputPath is empty. Keep
these tests narrow and separate from the existing integration success-path test.

Source: Coding guidelines

src/renderer/src/stores/collectionStore.ts (1)

153-161: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the scopeExcluded default out of the generic predicate.

buildFilterPredicate is a generic filter utility, but it hard-codes one Dynatrace field name. Any other collection that adds a boolean field with legacy null rows needs another special case here. Consider passing the defaulted fields through CollectionQueryOptions so the domain knowledge stays with the caller.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/stores/collectionStore.ts` around lines 153 - 161, Update
buildFilterPredicate to remove the hard-coded scopeExcluded fallback and keep
its predicate generic. Extend CollectionQueryOptions to carry fields requiring
an undefined-to-false default, then have the relevant caller configure
scopeExcluded there and apply that configuration when evaluating field values.
src/main/handlers/cacheHandlers.test.ts (1)

241-256: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Split the rejection cases so failures identify the broken branch.

The test asserts three distinct rejection paths with two shared assertions. If only one path regresses, the failure does not show which input was accepted. Use it.each with one case per row.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/handlers/cacheHandlers.test.ts` around lines 241 - 256, Split the
combined test around rejects invalid query identities and memberships into an
it.each table with one rejection input per row, covering the invalid read
identity, invalid snapshot identity, and invalid record ID cases. Keep the
shared expectations that readQueryMembership and writeQueryMembership are not
called within each parameterized case so failures identify the specific rejected
input.
src/renderer/src/stores/collectionStoreRegistry.ts (1)

93-103: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Guard the storeRef placeholder against a callback during construction.

entry.storeRef is undefined until line 122. The subscriber-count callback dereferences it at line 103 with entry.storeRef.deref(). The current CollectionStore constructor does not call the callback, so no crash occurs today. If a later change makes the constructor report an initial subscriber count, this throws a TypeError. Optional chaining removes the hazard.

🛡️ Proposed fix
-      const retainedStore = entry.strongStore ?? entry.storeRef.deref();
+      const retainedStore = entry.strongStore ?? entry.storeRef?.deref();

Apply the same guard at lines 113, 131, 142, and inside the finalization callback at line 33.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/stores/collectionStoreRegistry.ts` around lines 93 - 103,
Guard every dereference of the placeholder storeRef with optional chaining,
including the subscriber-count callback during CollectionStore construction, the
references around lines 113, 131, and 142, and the finalization callback.
Preserve the existing retained-store behavior while preventing callbacks from
throwing before storeRef is initialized.
src/renderer/src/tabs/dynatrace-problems.css (1)

321-337: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The two new button blocks duplicate the same declarations.

.dt-problems__history-pagination button and .dt-problem-note__ticket-actions button share border, border-radius, background, color, cursor, font, and font-weight, plus an identical hover rule. Only min-height, padding, and font-size differ.

Extract a shared secondary-button class, then override the three differing properties per context.

Also applies to: 1059-1075

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/tabs/dynatrace-problems.css` around lines 321 - 337, Extract
the shared button declarations and hover styling from
.dt-problems__history-pagination button and .dt-problem-note__ticket-actions
button into a reusable secondary-button class. Update both contexts to use that
class, retaining context-specific overrides only for min-height, padding, and
font-size.
src/main/dynatrace/DynatraceProblemsClient.test.ts (2)

313-334: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the Retry-After header path.

This test only covers error.retryAfterSeconds in the response body. retryAfterHeaderMilliseconds is checked first in retryAfterMilliseconds and handles both a numeric seconds value and an HTTP-date value. Neither branch is exercised.

💚 Suggested added case
+  it('prefers the Retry-After header over the response body', async () => {
+    const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
+      new Response(JSON.stringify({ error: { retryAfterSeconds: 5 } }), {
+        status: 429,
+        headers: { 'Content-Type': 'application/json', 'Retry-After': '30' },
+      }),
+    );
+    const client = new DynatraceProblemsClient(fetchMock);
+
+    const error = await client.testConnection(config).catch((caught) => caught);
+
+    expect(getDynatraceRetryAfterMs(error)).toBe(30_000);
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/dynatrace/DynatraceProblemsClient.test.ts` around lines 313 - 334,
Extend the rate-limit coverage around the test named “preserves Dynatrace retry
guidance on rate-limit errors” to exercise Retry-After response headers,
including both numeric-seconds and HTTP-date values. Assert the resulting retry
delay is preserved through getDynatraceRetryAfterMs, while keeping the existing
response-body retryAfterSeconds coverage intact.

299-311: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

The 10,000-record fixture makes this test expensive.

The test builds 10,000 problem objects, serializes them with JSON.stringify, parses the JSON back, and runs the full Zod problemSchema array parse. That is significant work for a single boolean assertion.

Export MAX_PROBLEMS from DynatraceProblemsClient.ts and assert against a smaller derived limit, or make the maximum injectable so the test can use a small value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/dynatrace/DynatraceProblemsClient.test.ts` around lines 299 - 311,
Export the existing maximum-problems limit as MAX_PROBLEMS from
DynatraceProblemsClient, then update the exact-limit test to derive its fixture
size from that constant or use an injectable smaller limit. Preserve the
assertion that a result exactly at the configured limit sets resultTruncated to
true without a notification, while avoiding construction and schema parsing of
10,000 records.
src/renderer/src/hooks/__tests__/useDynatraceProblems.test.ts (1)

85-140: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The test only covers the empty-problem path.

The useCollection mock returns data: [] for DYNATRACE_PROBLEMS_COLLECTION. loadedProblemIds is therefore always empty, and the assertions pin values: [] with enabled: false. The batching behavior that the change introduces, meaning non-empty values and enabled: true, is never exercised.

The mock also returns [state] and [historicalNote] for the related collections while those queries are disabled. A disabled query returns no data at runtime, so the fixture does not match real behavior.

Add a case where the mock returns open and history problems, then assert that batchedFilter.values contains the deduplicated problem IDs and that enabled is true.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/hooks/__tests__/useDynatraceProblems.test.ts` around lines
85 - 140, The test case around useDynatraceProblems currently covers only empty
problem results and disabled related queries. Add a non-empty fixture for open
and historical problems in the useCollection mock, ensure related collections
reflect disabled-query runtime behavior, and assert that both related
batchedFilter.values contain deduplicated problem IDs with enabled set to true.
src/renderer/src/tabs/__tests__/DynatraceProblemsTab.test.tsx (1)

236-255: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Confirm that Load 100 more matches HISTORY_PAGE_SIZE.

The test hardcodes the button label Load 100 more. HISTORY_PAGE_SIZE in src/renderer/src/hooks/useDynatraceProblems.ts is also 100. If the constant changes, the component label changes and this test fails for a reason unrelated to the behavior under test.

Import the page-size constant or match with a regular expression such as /Load \d+ more/.

Also applies to: 257-274

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/tabs/__tests__/DynatraceProblemsTab.test.tsx` around lines
236 - 255, Update the “loads more resolved history” test around the Load 100
more interaction to avoid hardcoding the current page size: import and use
HISTORY_PAGE_SIZE when constructing the button matcher, or use a numeric matcher
such as /Load \d+ more/. Apply the same change to the additional assertion range
noted in the comment, while preserving the existing loadMoreHistory behavior
check.
src/main/dynatrace/DynatraceProblemsClient.ts (1)

369-375: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

numericCount accepts an empty string as 0.

Number('') returns 0. An empty problemCount field therefore reports a valid count of zero instead of a parse failure. In inspectAlertingProfileField, problemCount === 0 forces healthy: true, so a malformed response is reported as healthy scope metadata.

♻️ Proposed guard
 function numericCount(record: Record<string, unknown> | undefined, field: string): number | null {
   const value = record?.[field];
-  const count = typeof value === 'string' ? Number(value) : value;
+  const count = typeof value === 'string' ? (value.trim() === '' ? NaN : Number(value)) : value;
   return typeof count === 'number' && Number.isFinite(count) && count >= 0
     ? Math.floor(count)
     : null;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/dynatrace/DynatraceProblemsClient.ts` around lines 369 - 375, Update
numericCount to reject empty or whitespace-only string values before converting
them with Number, returning null for those inputs. Preserve numeric handling and
valid non-empty numeric strings so inspectAlertingProfileField does not treat
malformed problemCount data as zero.
src/renderer/src/components/settings/AdministrationSettings.test.tsx (1)

270-317: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extend the test to cover the post-apply behavior.

replaceProfiles in src/renderer/src/components/settings/administration/RelayServerPanel.tsx closes the dialog and sets the feedback message when result.ok is true. This test stops at the execute assertion, so a regression that leaves the dialog open would still pass.

💚 Proposed additional assertions
     fireEvent.click(within(dialog).getByRole('button', { name: 'Apply stored scope' }));
     await waitFor(() =>
       expect(execute).toHaveBeenCalledWith({
         command: 'administration.setting.replace',
         payload: {
           setting: 'dynatrace.alerting-profiles',
           value: { profiles: ['NOC Core', 'Retail Stores'] },
           expectedRevision: 4,
         },
         expectedRevision: null,
       }),
     );
+    await waitFor(() =>
+      expect(
+        screen.queryByRole('dialog', { name: 'Review stored problem scope' }),
+      ).not.toBeInTheDocument(),
+    );
+    expect(screen.getByRole('status')).toHaveTextContent('Stored Dynatrace problem scope updated.');
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/settings/AdministrationSettings.test.tsx` around
lines 270 - 317, The test should also verify the successful post-apply behavior
of replaceProfiles: after the Apply stored scope action resolves successfully,
assert that the review dialog closes and the success feedback message is
displayed. Keep the existing execute payload assertion unchanged.
src/main/handlers/windowHandlers.test.ts (1)

483-492: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Extend the rejection cases to the remaining normalizeServiceDeskUrl guards.

The table covers protocol, credentials, port, leading whitespace, and a newline. Two guards in normalizeServiceDeskUrl stay untested: the MAX_EXTERNAL_URL_LENGTH bound and the non-string type check. The type check matters because IPC arguments are unvalidated at runtime.

Per coding guidelines: "Start with the narrowest tests that cover the changed behavior."

💚 Proposed additional cases
     it.each([
       'http://servicedesk.example.com/INC0012345',
       'https://user:secret@servicedesk.example.com/INC0012345',
       'https://servicedesk.example.com:8443/INC0012345',
       ' https://servicedesk.example.com/INC0012345',
       'https://servicedesk.example.com/INC0012345\nignored',
+      `https://servicedesk.example.com/${'a'.repeat(2_100)}`,
     ])('blocks unsafe Service Desk URL %s', async (url) => {
       await expect(getHandler(IPC_CHANNELS.OPEN_SERVICE_DESK_URL)({}, url)).resolves.toBe(false);
       expect(shell.openExternal).not.toHaveBeenCalled();
     });
+
+    it.each([undefined, null, 42, { href: 'https://servicedesk.example.com' }])(
+      'blocks non-string Service Desk input %p',
+      async (url) => {
+        await expect(getHandler(IPC_CHANNELS.OPEN_SERVICE_DESK_URL)({}, url)).resolves.toBe(false);
+        expect(shell.openExternal).not.toHaveBeenCalled();
+      },
+    );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/handlers/windowHandlers.test.ts` around lines 483 - 492, Extend the
unsafe Service Desk URL table in the test for
getHandler(IPC_CHANNELS.OPEN_SERVICE_DESK_URL) with cases covering
normalizeServiceDeskUrl’s MAX_EXTERNAL_URL_LENGTH limit and non-string inputs,
asserting each resolves false and shell.openExternal is not called.

Source: Coding guidelines

src/shared/urlSecurity.ts (1)

2-3: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Consider extending the rejected character class to format characters.

\p{Cc} matches C0 and C1 control characters only. It does not match \p{Cf} format characters such as U+200E, U+200F, and the bidi isolates U+2066-U+2069. An operator-supplied URL that carries bidi controls renders with a misleading direction in the UI label while pointing at a different host. The leading and trailing cases are already covered, because String.prototype.trim removes U+2028 and U+2029 and the check at line 122 rejects padded input.

🛡️ Proposed hardening
-const CONTROL_CHARACTER_PATTERN = /\p{Cc}/u;
+const CONTROL_CHARACTER_PATTERN = /[\p{Cc}\p{Cf}]/u;

Confirm this does not reject legitimate ticket URLs in your environment before you apply it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/urlSecurity.ts` around lines 2 - 3, Extend
CONTROL_CHARACTER_PATTERN to reject Unicode format characters alongside C0/C1
controls, covering bidi and directional formatting characters in URL validation.
Verify legitimate ticket URLs remain accepted in the target environment, and
keep the existing trimming and padded-input checks unchanged.
src/main/dynatrace/DynatraceProblemsManager.ts (2)

561-592: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Reconciliation updates run one record at a time over the full collection.

reconcileProblemScope loads every problem record, then awaits one update call per changed record in sequence. History is retained for one year, so this list can be large. After an operator changes the alerting-profile scope, the first reconciliation can mark a large fraction of records, which makes the loop long and blocks the sync from completing.

upsertProblems and deleteProblemsWithRelatedRecords already use the shared-iterator worker pattern with UPSERT_CONCURRENCY. Apply the same pattern here.

♻️ Proposed refactor to bound reconciliation latency
-    const excludedAt = new Date().toISOString();
-    let excludedCount = 0;
-    for (const problem of problems) {
-      const shouldExclude = Boolean(
-        selectedProfiles &&
-        !problem.alertingProfiles?.some((profile) => selectedProfiles.has(profile)),
-      );
-      if (shouldExclude) excludedCount += 1;
-      const missingExcludedAt = shouldExclude && parsedTimestamp(problem.scopeExcludedAt) === null;
-      const staleIncludedAt = !shouldExclude && Boolean(problem.scopeExcludedAt);
-      if (problem.scopeExcluded === shouldExclude && !missingExcludedAt && !staleIncludedAt)
-        continue;
-      await pb.collection(DYNATRACE_PROBLEMS_COLLECTION).update(
-        problem.id,
-        {
-          scopeExcluded: shouldExclude,
-          scopeExcludedAt: shouldExclude ? excludedAt : '',
-        },
-        { requestKey: null },
-      );
-    }
-    return excludedCount;
+    const excludedAt = new Date().toISOString();
+    let excludedCount = 0;
+    const pending: Array<{ id: string; shouldExclude: boolean }> = [];
+    for (const problem of problems) {
+      const shouldExclude = Boolean(
+        selectedProfiles &&
+          !problem.alertingProfiles?.some((profile) => selectedProfiles.has(profile)),
+      );
+      if (shouldExclude) excludedCount += 1;
+      const missingExcludedAt = shouldExclude && parsedTimestamp(problem.scopeExcludedAt) === null;
+      const staleIncludedAt = !shouldExclude && Boolean(problem.scopeExcludedAt);
+      if (problem.scopeExcluded === shouldExclude && !missingExcludedAt && !staleIncludedAt)
+        continue;
+      pending.push({ id: problem.id, shouldExclude });
+    }
+    // See upsertProblems: one shared iterator, N workers, no index bookkeeping.
+    const queue = pending[Symbol.iterator]();
+    const worker = async () => {
+      for (const { id, shouldExclude } of queue) {
+        await pb.collection(DYNATRACE_PROBLEMS_COLLECTION).update(
+          id,
+          {
+            scopeExcluded: shouldExclude,
+            scopeExcludedAt: shouldExclude ? excludedAt : '',
+          },
+          { requestKey: null },
+        );
+      }
+    };
+    await Promise.all(
+      Array.from({ length: Math.min(UPSERT_CONCURRENCY, pending.length) }, () => worker()),
+    );
+    return excludedCount;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/dynatrace/DynatraceProblemsManager.ts` around lines 561 - 592,
Refactor reconcileProblemScope to update changed problem records through the
shared bounded-concurrency iterator pattern used by upsertProblems and
deleteProblemsWithRelatedRecords, using UPSERT_CONCURRENCY. Preserve the
existing shouldExclude calculation, excludedCount result, update payload, and
request key behavior while allowing updates to run concurrently with the
configured limit.

369-387: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Retry delay does not grow with consecutive failures.

The failure path stores consecutiveFailures, but the retry delay stays at max(POLL_INTERVAL_MS, retryAfter) for every attempt. If Dynatrace stays unavailable and sends no Retry-After value, Relay keeps polling at the base interval indefinitely. Derive the delay from the new failure count so repeated failures back off, and keep the Dynatrace-provided value as a lower bound.

♻️ Proposed refactor to add bounded backoff
       const message = getErrorMessage(error);
-      const retryDelay = Math.max(POLL_INTERVAL_MS, getDynatraceRetryAfterMs(error) ?? 0);
+      const failures = (previousSync?.consecutiveFailures ?? 0) + 1;
+      const backoff = Math.min(POLL_INTERVAL_MS * 2 ** (failures - 1), MAX_RETRY_DELAY_MS);
+      const retryDelay = Math.max(backoff, getDynatraceRetryAfterMs(error) ?? 0);
       this.scheduledRetryAt = Date.now() + retryDelay;

Add the bound next to the other interval constants:

const MAX_RETRY_DELAY_MS = 30 * 60_000;

Note: the existing test at src/main/dynatrace/DynatraceProblemsManager.test.ts lines 361-452 advances timers by 61 seconds between attempts. Backoff would require that test to advance further.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/dynatrace/DynatraceProblemsManager.ts` around lines 369 - 387,
Update the catch path in DynatraceProblemsManager to calculate
consecutiveFailures before deriving retryDelay, then apply bounded exponential
backoff from POLL_INTERVAL_MS while retaining getDynatraceRetryAfterMs(error) as
a lower bound and capping the result with MAX_RETRY_DELAY_MS. Reuse that failure
count in writeSyncState, add the bound alongside the interval constants, and
adjust affected retry-timing tests to advance timers according to the backoff.
src/renderer/src/components/settings/administration/RelayServerPanel.tsx (1)

28-45: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Consider de-duplicating the parsed profile names.

selectedProfileNames keeps every non-empty line. If an operator repeats a profile name, the stored value contains a duplicate, and addedProfileNames can show that name twice in the review dialog. The manager builds a Set from the selection, so duplicates only affect the persisted value and the dialog copy.

♻️ Proposed refactor
   const selectedProfileNames = useMemo(
     () =>
-      profileText
-        .split(/\r?\n/)
-        .map((value) => value.trim())
-        .filter(Boolean),
+      [
+        ...new Set(
+          profileText
+            .split(/\r?\n/)
+            .map((value) => value.trim())
+            .filter(Boolean),
+        ),
+      ],
     [profileText],
   );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/settings/administration/RelayServerPanel.tsx`
around lines 28 - 45, Update selectedProfileNames in the RelayServerPanel
profile parsing flow to remove duplicate trimmed names before calculating
addedProfileNames and persisting the selection. Preserve the existing trimming
and empty-line filtering behavior, using a uniqueness-preserving collection or
equivalent.
src/renderer/src/tabs/DynatraceProblemsTab.tsx (1)

1467-1491: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

canSyncDynatrace is computed twice from the same expression.

Line 1471 computes the predicate inside handleRefresh, and line 1490 computes the identical expression for refreshControlCopy. The two copies can diverge, which would make the refresh button label describe an action the handler does not take. Hoist one const above handleRefresh and use it in both places.

♻️ Proposed refactor
+  const canSyncDynatrace = relayMode === 'server' && globalThis.api?.runtime?.kind !== 'web';
+
   const handleRefresh = async () => {
     if (savingAction) return;
     setSavingAction('refresh');
     try {
-      const canSyncDynatrace = relayMode === 'server' && globalThis.api?.runtime?.kind !== 'web';
       if (canSyncDynatrace && sync?.state !== 'disabled') {
   const lastSyncLabel = getLastSyncLabel(sync);
-  const canSyncDynatrace = relayMode === 'server' && globalThis.api?.runtime?.kind !== 'web';
   const refreshControl = refreshControlCopy(canSyncDynatrace, savingAction === 'refresh');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/tabs/DynatraceProblemsTab.tsx` around lines 1467 - 1491,
Hoist the shared canSyncDynatrace predicate above handleRefresh, then reuse that
constant both inside the refresh handler and when calling refreshControlCopy.
Remove the duplicate local declaration while preserving the existing behavior.
🔇 Additional comments (23)
src/main/handlers/cloudStatus/dynatraceStatusProvider.ts (1)

160-163: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

⚠️ Unverified finding
Sandbox verification was unavailable.

Enforce the payload limit while reading the response.

response.text() buffers the complete body before Line 161 checks its size. A response without a valid Content-Length can therefore allocate far more than MAX_RESPONSE_BYTES.

Read response.body through a reader. Count each chunk. Cancel and throw when the cumulative size exceeds the limit. Add a chunked-response test that verifies the reader stops at the limit.

Proposed fix
+async function readResponseTextWithinLimit(response: Response): Promise<string> {
+  const reader = response.body?.getReader();
+  if (!reader) return '';
+
+  const decoder = new TextDecoder();
+  let bytes = 0;
+  let text = '';
+  try {
+    for (;;) {
+      const { done, value } = await reader.read();
+      if (done) break;
+      bytes += value.byteLength;
+      if (bytes > MAX_RESPONSE_BYTES) {
+        await reader.cancel().catch(() => undefined);
+        throw new Error(`Dynatrace Status.io response exceeds ${MAX_RESPONSE_BYTES} bytes`);
+      }
+      text += decoder.decode(value, { stream: true });
+    }
+  } finally {
+    reader.releaseLock();
+  }
+  return text + decoder.decode();
+}
+
-  const responseBody = await response.text();
-  if (Buffer.byteLength(responseBody, 'utf8') > MAX_RESPONSE_BYTES) {
-    throw new Error(`Dynatrace Status.io response exceeds ${MAX_RESPONSE_BYTES} bytes`);
-  }
+  const responseBody = await readResponseTextWithinLimit(response);

As per coding guidelines, start with the narrowest tests that cover the changed behavior.

src/renderer/src/utils/__tests__/cloudStatusDisplay.test.ts (1)

35-118: 📐 Maintainability & Code Quality

⚠️ Unverified finding
Sandbox verification was unavailable.

Run focused validation before a readiness claim.

Run the changed cloud-status renderer tests first.

As per coding guidelines, “Start with the narrowest tests that cover the changed behavior” and run npm audit --audit-level=high --omit=dev before a readiness or push-to-test claim.

src/renderer/src/utils/cloudStatus.ts (1)

5-5: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

⚠️ Unverified finding
Sandbox verification was unavailable.

Remove the duplicate CloudIssueCandidate declarations.

Line 5 declares CloudIssueCandidate three times. TypeScript reports a duplicate identifier and stops the renderer type check. Keep one declaration.

Proposed fix
 type CloudIssueCandidate = { severity: CloudStatusSeverity; pubDate: string };
-type CloudIssueCandidate = { severity: CloudStatusSeverity; pubDate: string };
-type CloudIssueCandidate = { severity: CloudStatusSeverity; pubDate: string };
.github/workflows/release.yml (1)

35-84: 🩺 Stability & Availability

⚠️ Unverified finding
Sandbox verification was unavailable.

Confirm the required checks come from the github-actions app.

Line 53 keeps only check runs whose app.slug equals github-actions. If SonarQube quality gate or Snyk security gate is reported by the SonarQube or Snyk GitHub App instead of an Actions job, the filter removes it. The loop then waits the full 25 minutes and the release fails on every test push.

Also confirm the three names match the workflow job name values exactly, because the map is keyed on the check-run name.

package.json (1)

16-16: 📐 Maintainability & Code Quality

⚠️ Unverified finding
Sandbox verification was unavailable.

Check for stale references to the removed release script.

The release script no longer exists. Any workflow, script, or document that calls npm run release now fails.

scripts/release-version.test.mjs (1)

15-15: 🩺 Stability & Availability

⚠️ Unverified finding
Sandbox verification was unavailable.

Confirm the pinned Node version supports import.meta.dirname.

import.meta.dirname was added in Node 20.11 and 21.2. If .node-version pins an earlier release, this value is undefined and path.resolve throws.

src/renderer/src/hooks/__tests__/useCollection.test.ts (1)

359-374: 📐 Maintainability & Code Quality | ⚡ Quick win

⚠️ Unverified finding
Sandbox verification was unavailable.

Confirm that globalThis.api is reset between tests.

These tests assign globalThis.api directly. If no afterEach deletes it, later tests inherit cache mocks from earlier tests and can pass for the wrong reason. Verify that a cleanup hook exists in this file or the shared setup.

src/main/cache/OfflineCache.test.ts (1)

277-296: 📐 Maintainability & Code Quality | 💤 Low value

⚠️ Unverified finding
Sandbox verification was unavailable.

Confirm that Date.now is restored if an assertion fails.

The test restores the spy with now.mockRestore() inside the test body. If a later write throws, Date.now stays mocked at 64 for the rest of the suite. Confirm that the suite config sets restoreMocks: true, or that the existing afterEach calls vi.restoreAllMocks().

♻️ Alternative that always restores
-    const now = vi.spyOn(Date, 'now');
-    for (let index = 0; index < 65; index += 1) {
-      now.mockReturnValue(index);
+    vi.useFakeTimers();
+    for (let index = 0; index < 65; index += 1) {
+      vi.setSystemTime(index);
       cache.writeQueryMembership('dynatrace_problems', index.toString(16).padStart(16, '0'), {
         recordIds: [`problem-${index}`],
         totalItems: 1,
         complete: true,
       });
     }
-    now.mockRestore();
+    vi.useRealTimers();
src/renderer/src/runtime/WebBridge.test.ts (1)

368-385: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

⚠️ Unverified finding
Sandbox verification was unavailable.

Consider adding a javascript: scheme case.

The test covers HTTP and userinfo rejection. A scheme-injection case is the highest-risk input for an external-open path. If src/shared/urlSecurity.ts tests already cover javascript: for normalizeServiceDeskUrl, no change is needed here.

src/main/pocketbase/schema/collectionCatalog.ts (1)

1005-1010: 🗄️ Data Integrity & Integration | ⚡ Quick win

⚠️ Unverified finding
Sandbox verification was unavailable.

Verify that existing dynatrace_problems rows resolve scopeExcluded to false.

src/renderer/src/hooks/useDynatraceProblems.ts filters both queries with scopeExcluded=false. scopeExcluded is a new bool field on an existing collection. If PocketBase leaves the column NULL for rows that already exist, the filter excludes them and every previously synced problem disappears from the Open and History views until the next sync rewrites the record.

Confirm that the collection bootstrap applies the field with a false value for existing rows, or that the Dynatrace manager backfills it.

src/main/dynatrace/DynatraceProblemsClient.ts (1)

349-356: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

⚠️ Unverified finding
Sandbox verification was unavailable.

parseQueryResult throws on in-progress poll payloads.

parseQueryResult tolerates only null and undefined. Any other value must satisfy queryResultSchema, which requires a records array. runQuery calls parseQueryResult(response.result) on every loop iteration, including RUNNING responses. If Dynatrace returns a non-null progress object without records while the query is still executing, the client throws Dynatrace returned an unexpected Grail query result. and abandons a query that would have succeeded.

Parse the result only when the state is SUCCEEDED, or treat a parse failure on a non-terminal state as "not ready yet".

🐛 Proposed fix
-function parseQueryResult(result: unknown): QueryResult | null {
-  if (result == null) return null;
-  const parsed = queryResultSchema.safeParse(result);
-  if (!parsed.success) {
-    throw new Error('Dynatrace returned an unexpected Grail query result.');
-  }
-  return parsed.data;
-}
+function parseQueryResult(result: unknown, terminal: boolean): QueryResult | null {
+  if (result == null) return null;
+  const parsed = queryResultSchema.safeParse(result);
+  if (!parsed.success) {
+    if (!terminal) return null;
+    throw new Error('Dynatrace returned an unexpected Grail query result.');
+  }
+  return parsed.data;
+}

Then update the call site:

-      const result = parseQueryResult(response.result);
-      if (response.state === 'SUCCEEDED' && result) return result;
+      const terminal = response.state === 'SUCCEEDED';
+      const result = parseQueryResult(response.result, terminal);
+      if (terminal && result) return result;
src/renderer/src/hooks/useDynatraceProblems.ts (4)

39-58: 🗄️ Data Integrity & Integration | ⚡ Quick win

⚠️ Unverified finding
Sandbox verification was unavailable.

Two different collections share one batchedFilter.key.

states queries DYNATRACE_PROBLEM_STATES_COLLECTION and notes queries DYNATRACE_PROBLEM_NOTES_COLLECTION, but both pass key: 'dynatrace-loaded-problems'. If collectionStoreRegistry scopes batch state by key alone, the two queries share one batch entry and one result set. If it scopes by collection plus key, the shared key is safe.

Confirm the scoping rule. If the key is global, give each query a distinct key.


35-58: 🚀 Performance & Scalability | 🏗️ Heavy lift

⚠️ Unverified finding
Sandbox verification was unavailable.

History pagination re-batches every related query.

loadedProblemIds is derived from the combined open and history list. Each loadMoreHistory call adds up to HISTORY_PAGE_SIZE (100) IDs. That changes batchedFilter.values for both states and notes, with RELATED_PROBLEM_BATCH_SIZE of 40 per batch.

If the batching layer refetches all batches when values changes, page n costs ceil(100n / 40) requests per related collection. Loading five history pages then issues 13 state requests and 13 note requests for that page alone, and re-reads all previously fetched IDs.

Confirm that the batching layer fetches only the new IDs. If it does not, pass only the newly added IDs, or scope related lookups to the visible page.


105-113: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

⚠️ Unverified finding
Sandbox verification was unavailable.

refetch changes identity on every render.

The dependency array lists the collection result objects, not their refetch functions. useCollection returns a new object on each render, so refetch is a new function on each render. Any consumer that puts refetch in a useEffect dependency array runs an effect loop.

Depend on the refetch functions instead.

♻️ Proposed fix
   const refetch = useCallback(async () => {
     await Promise.all([
       openProblems.refetch(),
       historyProblems.refetch(),
       states.refetch(),
       notes.refetch(),
       sync.refetch(),
     ]);
-  }, [historyProblems, notes, openProblems, states, sync]);
+  }, [
+    historyProblems.refetch,
+    notes.refetch,
+    openProblems.refetch,
+    states.refetch,
+    sync.refetch,
+  ]);

This helps only if useCollection returns a stable refetch. Confirm that it wraps refetch in useCallback.


115-131: 🩺 Stability & Availability | ⚡ Quick win

⚠️ Unverified finding
Sandbox verification was unavailable.

loading stays true if a disabled collection reports loading.

states and notes are created with enabled: loadedProblemIds.length > 0. On first render there are no problems, so both are disabled. loading ORs states.loading and notes.loading. If useCollection reports loading: true for a disabled query, the tab shows a loading state that never clears when the problem list is legitimately empty.

The test mock in src/renderer/src/hooks/__tests__/useDynatraceProblems.test.ts always returns loading: false, so this path is not covered.

Confirm that a disabled useCollection reports loading: false.

src/renderer/src/tabs/__tests__/DynatraceProblemsTab.test.tsx (2)

174-183: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

⚠️ Unverified finding
Sandbox verification was unavailable.

Remove the duplicated assignment and restore runtime after the test.

Two issues at this site:

  1. The assignment to globalThis.api.runtime appears twice in a row. The second statement is redundant.
  2. The test sets runtime to { kind: 'web' } and never restores it. Every test that runs after this one sees the web runtime. That changes which toolbar action the tab renders and whether syncDynatraceProblems is reachable.
🐛 Proposed fix
   it('reloads Relay data without requesting a privileged sync in Relay Web', async () => {
     if (!globalThis.api) throw new Error('Expected bridge fixture');
-    (globalThis.api as unknown as { runtime: { kind: 'web' } }).runtime = { kind: 'web' };
-    (globalThis.api as unknown as { runtime: { kind: 'web' } }).runtime = { kind: 'web' };
+    const bridge = globalThis.api as unknown as { runtime: { kind: string } };
+    const previousRuntime = bridge.runtime;
+    bridge.runtime = { kind: 'web' };
+    onTestFinished(() => {
+      bridge.runtime = previousRuntime;
+    });
 
     render(<DynatraceProblemsTab relayMode="server" />);

onTestFinished is imported from vitest. If a beforeEach already rebuilds globalThis.api, only the duplicated line needs removal.


205-274: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

⚠️ Unverified finding
Sandbox verification was unavailable.

Each test spreads the mutated mocks.hookValue, so fixture state accumulates.

mocks.hookValue is a module-level object. Every test here reassigns it with { ...mocks.hookValue, ... }. Unless a beforeEach restores the base fixture, state leaks forward:

  • The truncation test at Line 221 inherits state: 'syncing' from the test at Line 205.
  • The cached-history test at Line 257 inherits hasMoreHistory: true and totalHistoryCount: 250 from the test at Line 236.

The assertions still pass because each test checks only its own field. The tests are therefore not independent, and a future assertion added to one of them can fail for an unrelated reason.

Build each fixture from a fresh base constant rather than from the current mocks.hookValue.

src/renderer/src/tabs/dynatrace-problems.css (2)

178-182: 📐 Maintainability & Code Quality | ⚡ Quick win

⚠️ Unverified finding
Sandbox verification was unavailable.

Verify the contrast ratio of the warning notice.

The rule sets color: var(--color-warning) on background: var(--color-warning-subtle). A warning hue placed on its own tinted background often falls below the WCAG 2.1 AA requirement of 4.5:1 for body text. The test at Line 232 of src/renderer/src/tabs/__tests__/DynatraceProblemsTab.test.tsx asserts this notice uses role="alert", so the text must be readable.


649-649: 📐 Maintainability & Code Quality | 💤 Low value

⚠️ Unverified finding
Sandbox verification was unavailable.

Confirm the fact-item count matches the three-column grid.

The grid changes from four columns to three. If the detail panel still renders four fact items, the fourth item wraps onto a second row and occupies one third of the width.

src/main/dynatrace/DynatraceProblemsManager.test.ts (1)

583-606: 🩺 Stability & Availability

⚠️ Unverified finding
Sandbox verification was unavailable.

Confirm the retry-guidance fixture matches the real client error shape.

The fixture passes { cause: 90_000 }, and the assertion expects nextRetryAt 90 seconds after the fake clock. This encodes the assumption that getDynatraceRetryAfterMs reads a plain number from error.cause. If DynatraceProblemsClient attaches Retry-After in a different shape, this test passes while production retry guidance is discarded.

src/renderer/src/components/settings/administration/RelayServerPanel.tsx (1)

217-223: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

⚠️ Unverified finding
Sandbox verification was unavailable.

The panel provides no way to clear the stored problem scope.

disabled={!profiles || selectedProfileNames.length === 0} blocks submission when the textarea is empty. DynatraceProblemsManager.prepareProfileScope treats a null selection as unscoped and skips both the profile-health check and reconcileProblemScope exclusion. After an operator sets any profile list, this control offers no path back to the unscoped state.

Confirm the intended behavior. If operators must be able to remove the scope, allow an empty submission and map it to the unscoped value.

src/main/handlers/window/externalLinkHandlers.ts (1)

148-152: 🩺 Stability & Availability

⚠️ Unverified finding
Sandbox verification was unavailable.

Confirm describeUrlForLog tolerates non-string input.

The url: string annotation is a compile-time claim only. IPC arguments arrive from the renderer at runtime, so a compromised or buggy caller can send a number, object, or undefined. normalizeServiceDeskUrl handles that case and returns null. The rejection path then calls describeUrlForLog(url) with the same unchecked value. If describeUrlForLog calls a string method or new URL without a guard, this handler rejects its promise instead of returning false.

src/renderer/src/tabs/DynatraceProblemsTab.tsx (1)

1351-1359: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

⚠️ Unverified finding
Sandbox verification was unavailable.

Optional chaining guards api but not openServiceDeskUrl.

globalThis.api?.openServiceDeskUrl(url) only protects against a missing api object. In a runtime where api exists but does not expose openServiceDeskUrl, this line throws TypeError: ... is not a function. The throw happens inside the async callback, so the showToast error path never runs and the operator sees no feedback.

handleOpenDynatrace at lines 1332-1340 has the same shape for openExternal, but that method predates this change. openServiceDeskUrl is new, and this stack adds the Relay Web implementation in a separate file. Confirm every runtime bridge exposes it, or call it through an optional method access.

🛡️ Proposed defensive fix
   const handleOpenTicket = useCallback(
     async (reference: string) => {
       const url = getSafeTicketUrl(reference);
-      if (!url || !(await globalThis.api?.openServiceDeskUrl(url))) {
+      if (!url || !(await globalThis.api?.openServiceDeskUrl?.(url))) {
         showToast('Unable to open the Service Desk reference.', 'error');
       }
     },
     [showToast],
   );
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/SECURITY.md`:
- Around line 108-113: Update the Service Desk security description to
explicitly state that its handler accepts any HTTPS host, subject only to the
documented URL shape, credential, port, sender-validation, rate-limit, and
logging checks; clarify that the general OPEN_EXTERNAL host allowlist does not
apply.

In `@scripts/release-version.mjs`:
- Around line 121-125: Update runGit to pass an explicit maxBuffer option to
execFileSync, sized to accommodate readCommits output for full repository
histories when the range is HEAD, while preserving the existing cwd and UTF-8
encoding behavior.

In `@src/renderer/src/components/settings/administration/RelayServerPanel.tsx`:
- Around line 245-258: Update the RelayServerPanel apply-confirmation flow
around replaceProfiles to track its in-flight state, disable the “Apply stored
scope” and “Cancel” footer buttons while the replacement command runs, and
preserve the existing reauthentication loading pattern. Ensure rapid clicks
cannot dispatch multiple replaceProfiles commands with the same revision.

In `@src/renderer/src/stores/collectionStore.ts`:
- Around line 713-732: Update fetchOfflineSnapshot so a successful current
cached read passes error: null to updateSnapshot, matching fetchOnlineSnapshot.
Preserve the existing cached data, pagination, and membership metadata behavior
while clearing any stale error alongside valid offline data.

In `@src/renderer/src/tabs/DynatraceProblemsTab.tsx`:
- Around line 1071-1092: Update the Service Desk ticket composer help text near
the “Service Desk ticket number” field to state that entering a full HTTPS
ticket URL enables the “Open ↗” action, while preserving the existing guidance
for ticket numbers.

---

Nitpick comments:
In @.github/workflows/release.yml:
- Around line 201-222: Update the “Verify existing release assets” step to
enable Bash pipefail alongside nounset without adding errexit, preserving the
current conditional verification flow. Add cleanup for the temporary verify_dir
so downloaded assets are removed after the verification attempt, including when
the step completes through either branch.

In `@scripts/release-version.test.mjs`:
- Around line 162-177: Isolate the temporary Git repository from system and
global configuration in the test around the git commit setup. Set
GIT_CONFIG_GLOBAL and GIT_CONFIG_SYSTEM to nonexistent paths in the spawned Git
environment, or disable signing directly on the commit invocation with
commit.gpgsign=false, so global templates, hooks, signing, and passphrase
prompts cannot affect the fixture.
- Around line 179-202: Add focused unit tests for the rejection paths in
planRelease and writeGitHubOutputs: verify planRelease throws for short and
uppercase headSha values, and verify writeGitHubOutputs throws when action is
malformed or outputPath is empty. Keep these tests narrow and separate from the
existing integration success-path test.

In `@src/main/dynatrace/DynatraceProblemsClient.test.ts`:
- Around line 313-334: Extend the rate-limit coverage around the test named
“preserves Dynatrace retry guidance on rate-limit errors” to exercise
Retry-After response headers, including both numeric-seconds and HTTP-date
values. Assert the resulting retry delay is preserved through
getDynatraceRetryAfterMs, while keeping the existing response-body
retryAfterSeconds coverage intact.
- Around line 299-311: Export the existing maximum-problems limit as
MAX_PROBLEMS from DynatraceProblemsClient, then update the exact-limit test to
derive its fixture size from that constant or use an injectable smaller limit.
Preserve the assertion that a result exactly at the configured limit sets
resultTruncated to true without a notification, while avoiding construction and
schema parsing of 10,000 records.

In `@src/main/dynatrace/DynatraceProblemsClient.ts`:
- Around line 369-375: Update numericCount to reject empty or whitespace-only
string values before converting them with Number, returning null for those
inputs. Preserve numeric handling and valid non-empty numeric strings so
inspectAlertingProfileField does not treat malformed problemCount data as zero.

In `@src/main/dynatrace/DynatraceProblemsManager.ts`:
- Around line 561-592: Refactor reconcileProblemScope to update changed problem
records through the shared bounded-concurrency iterator pattern used by
upsertProblems and deleteProblemsWithRelatedRecords, using UPSERT_CONCURRENCY.
Preserve the existing shouldExclude calculation, excludedCount result, update
payload, and request key behavior while allowing updates to run concurrently
with the configured limit.
- Around line 369-387: Update the catch path in DynatraceProblemsManager to
calculate consecutiveFailures before deriving retryDelay, then apply bounded
exponential backoff from POLL_INTERVAL_MS while retaining
getDynatraceRetryAfterMs(error) as a lower bound and capping the result with
MAX_RETRY_DELAY_MS. Reuse that failure count in writeSyncState, add the bound
alongside the interval constants, and adjust affected retry-timing tests to
advance timers according to the backoff.

In `@src/main/handlers/cacheHandlers.test.ts`:
- Around line 241-256: Split the combined test around rejects invalid query
identities and memberships into an it.each table with one rejection input per
row, covering the invalid read identity, invalid snapshot identity, and invalid
record ID cases. Keep the shared expectations that readQueryMembership and
writeQueryMembership are not called within each parameterized case so failures
identify the specific rejected input.

In `@src/main/handlers/windowHandlers.test.ts`:
- Around line 483-492: Extend the unsafe Service Desk URL table in the test for
getHandler(IPC_CHANNELS.OPEN_SERVICE_DESK_URL) with cases covering
normalizeServiceDeskUrl’s MAX_EXTERNAL_URL_LENGTH limit and non-string inputs,
asserting each resolves false and shell.openExternal is not called.

In `@src/renderer/src/components/settings/administration/RelayServerPanel.tsx`:
- Around line 28-45: Update selectedProfileNames in the RelayServerPanel profile
parsing flow to remove duplicate trimmed names before calculating
addedProfileNames and persisting the selection. Preserve the existing trimming
and empty-line filtering behavior, using a uniqueness-preserving collection or
equivalent.

In `@src/renderer/src/components/settings/AdministrationSettings.test.tsx`:
- Around line 270-317: The test should also verify the successful post-apply
behavior of replaceProfiles: after the Apply stored scope action resolves
successfully, assert that the review dialog closes and the success feedback
message is displayed. Keep the existing execute payload assertion unchanged.

In `@src/renderer/src/hooks/__tests__/useDynatraceProblems.test.ts`:
- Around line 85-140: The test case around useDynatraceProblems currently covers
only empty problem results and disabled related queries. Add a non-empty fixture
for open and historical problems in the useCollection mock, ensure related
collections reflect disabled-query runtime behavior, and assert that both
related batchedFilter.values contain deduplicated problem IDs with enabled set
to true.

In `@src/renderer/src/stores/collectionStore.ts`:
- Around line 153-161: Update buildFilterPredicate to remove the hard-coded
scopeExcluded fallback and keep its predicate generic. Extend
CollectionQueryOptions to carry fields requiring an undefined-to-false default,
then have the relevant caller configure scopeExcluded there and apply that
configuration when evaluating field values.

In `@src/renderer/src/stores/collectionStoreRegistry.ts`:
- Around line 93-103: Guard every dereference of the placeholder storeRef with
optional chaining, including the subscriber-count callback during
CollectionStore construction, the references around lines 113, 131, and 142, and
the finalization callback. Preserve the existing retained-store behavior while
preventing callbacks from throwing before storeRef is initialized.

In `@src/renderer/src/tabs/__tests__/DynatraceProblemsTab.test.tsx`:
- Around line 236-255: Update the “loads more resolved history” test around the
Load 100 more interaction to avoid hardcoding the current page size: import and
use HISTORY_PAGE_SIZE when constructing the button matcher, or use a numeric
matcher such as /Load \d+ more/. Apply the same change to the additional
assertion range noted in the comment, while preserving the existing
loadMoreHistory behavior check.

In `@src/renderer/src/tabs/dynatrace-problems.css`:
- Around line 321-337: Extract the shared button declarations and hover styling
from .dt-problems__history-pagination button and
.dt-problem-note__ticket-actions button into a reusable secondary-button class.
Update both contexts to use that class, retaining context-specific overrides
only for min-height, padding, and font-size.

In `@src/renderer/src/tabs/DynatraceProblemsTab.tsx`:
- Around line 1467-1491: Hoist the shared canSyncDynatrace predicate above
handleRefresh, then reuse that constant both inside the refresh handler and when
calling refreshControlCopy. Remove the duplicate local declaration while
preserving the existing behavior.

In `@src/shared/urlSecurity.ts`:
- Around line 2-3: Extend CONTROL_CHARACTER_PATTERN to reject Unicode format
characters alongside C0/C1 controls, covering bidi and directional formatting
characters in URL validation. Verify legitimate ticket URLs remain accepted in
the target environment, and keep the existing trimming and padded-input checks
unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

Comment thread docs/SECURITY.md
Comment thread scripts/release-version.mjs
Comment thread src/renderer/src/stores/collectionStore.ts
Comment thread src/renderer/src/tabs/DynatraceProblemsTab.tsx
@sonarqubecloud

Copy link
Copy Markdown

@CrimsonSoul
CrimsonSoul merged commit c2cbfd2 into test Aug 12, 2026
7 checks passed
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.

1 participant