Skip to content

fix(site): defer external app API key generation to on-click - #27907

Open
jakehwll wants to merge 3 commits into
mainfrom
jakehwll/devex-460-defer-api-key-generation-in-useapplink-to-on-click-instead
Open

fix(site): defer external app API key generation to on-click#27907
jakehwll wants to merge 3 commits into
mainfrom
jakehwll/devex-460-defer-api-key-generation-in-useapplink-to-on-click-instead

Conversation

@jakehwll

@jakehwll jakehwll commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

🤖 This PR was written by Coder Agents on behalf of Jake Howell.

Resolves DEVEX-460.

Problem

useAppLink minted a session key on mount via a useQuery:

const { data: apiKeyResponse } = useQuery({
  ...apiKey(),
  enabled: isExternalApp(app) && needsSessionToken(app),
});

Whenever any page mounted useAppLink for an external app that embeds $SESSION_TOKEN in its URL (JetBrains Gateway, Coder Desktop, etc.), it fired POST /api/v2/users/me/keys on render, even if the user never clicked the link. Each call minted a fresh session key and produced a created token audit-log entry. Simply navigating the dashboard generated a stream of created token entries with no real connection activity.

This is the follow-up to #22318 (AIGOV-24), which only fixed the built-in VS Code / VS Code Insiders buttons in WorkspacesTable.

Change

Mirror the on-click minting pattern from #22318, applied to the shared useAppLink hook:

  • Replace the eager useQuery(apiKey()) with a useMutation(() => API.getApiKey()) that runs only when the user clicks a token-bearing external app. onClick mints the key, builds the final URL, and navigates via location.href (these are always custom-protocol, non-HTTP external apps).
  • The returned href no longer embeds a token; the hook owns opening for token apps.
  • Replace the hasToken field with isLoading (mint in-flight) and update call sites so token apps are always clickable, with a loading affordance while a key is being minted.
  • TaskApps external-app menu items now render an anchor with the hook's onClick (instead of a RouterLink that bypassed it), so the deferred mint runs there too.

No API key is minted until the user actually opens an external app.

Affected call sites

  • site/src/modules/apps/useAppLink.ts (core change)
  • site/src/modules/resources/AppLink/AppLink.tsx
  • site/src/pages/AgentsPage/components/WorkspacePill.tsx
  • site/src/pages/TaskPage/TaskApps.tsx

Testing

Added Storybook interaction tests (play functions) in AppLink.stories.tsx that assert the fix directly:

  • ExternalAppDefersSessionToken: a token-bearing external app mints no key on render, and mints exactly one on click.
  • ExternalAppWithoutSessionTokenNeverMints: an external app without $SESSION_TOKEN never mints a key, even on click.

Also verified:

  • tsc -p . (frontend typecheck) passes
  • biome check on changed files passes
  • vitest for AppLink (incl. new stories), apps, WorkspacePill, TaskApps, and AppStatuses stories/tests pass
  • pre-commit hook (gen/fmt/lint/build) passes

Manual verification checklist (recommended before merge)

  • With an external app that uses $SESSION_TOKEN (e.g. JetBrains Gateway), open DevTools → Network filtered on users/me/keys, reload /workspaces and the workspace detail page, and confirm no POST fires on render.
  • Confirm a POST fires only when the app link is clicked, and the app opens correctly.

@linear-code

linear-code Bot commented Aug 6, 2026

Copy link
Copy Markdown

DEVEX-460

@jakehwll jakehwll changed the title 🤖 fix(site/src): defer external app API key generation to on-click fix(site/src): defer external app API key generation to on-click Aug 6, 2026
@jakehwll
jakehwll requested a review from ethanndickson August 6, 2026 05:24
@jakehwll
jakehwll marked this pull request as ready for review August 6, 2026 05:25
@jakehwll jakehwll changed the title fix(site/src): defer external app API key generation to on-click fix(site): defer external app API key generation to on-click Aug 6, 2026

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3f150788c4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +114 to +118
generateKeyMutation.mutate(undefined, {
onSuccess: ({ key }) => {
notifyOnOpenExternalAppFailed();
location.href = buildHref(key);
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep token callbacks outside closing menu items

In the TaskApps and WorkspacePill dropdowns, selecting an item closes and unmounts ExternalAppMenuItem or AppMenuItem; when the POST outlives that close, TanStack Query drops the per-call callbacks passed to mutate after the observer loses its listeners. The key is then minted, but neither the success navigation nor the failure toast runs. Put these callbacks in the useMutation options or hoist the mutation above the dropdown so token-bearing apps open reliably and errors remain visible.

AGENTS.md reference: site/AGENTS.md:L16-L17

Useful? React with 👍 / 👎.

Comment on lines +57 to +60
// For apps that require a session token this href intentionally omits the
// token; the `onClick` handler mints one and navigates to the final URL.
// Callers still render it as an anchor for apps that don't need a token.
const href = buildHref("");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not expose a tokenless href for token-backed apps

For an app that requires a session token, this removes $SESSION_TOKEN and still exposes the result as a real anchor URL. Middle-clicking, choosing the browser's native Open Link action, or otherwise following the anchor without React's onClick launches the custom protocol with an empty token, so the app cannot authenticate and no key is generated. Token-backed actions should not expose the incomplete URL as a navigable href.

Useful? React with 👍 / 👎.

Comment on lines +285 to +287
const getApiKey = spyOn(API, "getApiKey").mockImplementation(
() => new Promise(() => {}),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Install the API spy before rendering the story

Storybook invokes play after the component has rendered and its effects have run, so installing the spy here cannot observe API calls made during mounting. A regression back to eager key fetching can therefore issue getApiKey() before this spy exists, while the subsequent not.toHaveBeenCalled() assertion still passes. Set up the mock before rendering so this story actually covers the deferred-fetch invariant required by FE1.

AGENTS.md reference: site/AGENTS.md:L9-L10

Useful? React with 👍 / 👎.

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.

2 participants