Skip to content

ref(onboarding): Convert CreateSampleEventButton to functional component#115830

Merged
ryan953 merged 4 commits into
masterfrom
ryan953/ref/convert-create-sample-event-button
May 20, 2026
Merged

ref(onboarding): Convert CreateSampleEventButton to functional component#115830
ryan953 merged 4 commits into
masterfrom
ryan953/ref/convert-create-sample-event-button

Conversation

@ryan953
Copy link
Copy Markdown
Member

@ryan953 ryan953 commented May 19, 2026

Convert CreateSampleEventButton from a class component with withApi/withOrganization HOCs to a functional component using hooks. No behavior change.

API modernization

Replace api.requestPromise with TanStack Query primitives: useMutation + fetchMutation for the POST, and queryClient.fetchQuery with apiOptions, retry, and retryDelay for polling the latest event. The manual while-loop with setTimeout is removed entirely. Replace browserHistory.push with useNavigate. Switch from default export to named export.

Polling via fetchQuery

The create-and-poll lifecycle lives in a single useMutation. After the POST creates the sample group, queryClient.fetchQuery polls the latest-event endpoint using built-in retry/retryDelay options instead of an imperative loop. This eliminates the _isMounted guard — useMutation handles unmount cleanup automatically.

View Interactive Summary

Replace class component with function component using hooks. Replace
withApi/withOrganization HOCs with useApi/useOrganization hooks. Convert
api.requestPromise POST call to useMutation with fetchMutation, and the
polling loop to useQuery with retry/retryDelay. Replace browserHistory
with useNavigate. Switch from default export to named export.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions github-actions Bot added the Scope: Frontend Automatically applied to PRs that change frontend components label May 19, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 19, 2026

📊 Type Coverage Diff

✅ No new type safety issues introduced. Coverage: 93.56%

Replace the imperative api.requestPromise polling loop and useQuery
bridge with queryClient.fetchQuery inside the mutation. This keeps the
entire create-and-poll lifecycle in a single useMutation, using
fetchQuery's built-in retry/retryDelay for polling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move fetchQuery polling out of mutationFn into onSuccess so the
mutation completes after the POST. Polling runs as a detached
.then()/.catch() chain, keeping isPending tied only to the sample
event creation request.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ryan953 ryan953 marked this pull request as ready for review May 19, 2026 22:15
@ryan953 ryan953 requested a review from a team as a code owner May 19, 2026 22:15
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit efdf737. Configure here.

Comment thread static/app/views/onboarding/createSampleEventButton.tsx
Comment thread static/app/views/onboarding/createSampleEventButton.tsx
Comment thread static/app/views/onboarding/createSampleEventButton.tsx
Add an AbortController to cancel the fetchQuery polling when the
component unmounts or when a new sample event is created. This
prevents stale navigate, toast, and analytics calls from firing
after the user has left the page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ryan953 ryan953 merged commit aa29bc0 into master May 20, 2026
70 of 72 checks passed
@ryan953 ryan953 deleted the ryan953/ref/convert-create-sample-event-button branch May 20, 2026 17:11
JonasBa pushed a commit that referenced this pull request May 21, 2026
…ent (#115830)

Convert `CreateSampleEventButton` from a class component with
`withApi`/`withOrganization` HOCs to a functional component using hooks.
No behavior change.

**API modernization**

Replace `api.requestPromise` with TanStack Query primitives:
`useMutation` + `fetchMutation` for the POST, and
`queryClient.fetchQuery` with `apiOptions`, `retry`, and `retryDelay`
for polling the latest event. The manual `while`-loop with `setTimeout`
is removed entirely. Replace `browserHistory.push` with `useNavigate`.
Switch from default export to named export.

**Polling via fetchQuery**

The create-and-poll lifecycle lives in a single `useMutation`. After the
POST creates the sample group, `queryClient.fetchQuery` polls the
latest-event endpoint using built-in `retry`/`retryDelay` options
instead of an imperative loop. This eliminates the `_isMounted` guard —
`useMutation` handles unmount cleanup automatically.


[![View Interactive
Summary](https://img.shields.io/badge/Summary-html_explanation-D97757?style=for-the-badge&logo=claude&logoColor=D97757)](https://insecure-html-azure.vercel.app/?pr-url=https%3A%2F%2Fgithub.com%2Fgetsentry%2Fsentry%2Fpull%2F115830)
<!-- html-preview:start
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>PR #115830 — ref(onboarding): Convert CreateSampleEventButton to
functional component</title>
<style>

:root{--bg:#fff;--surface:#f6f8fa;--border:#d0d7de;--text:#1f2328;--text-muted:#656d76;--add-bg:#dafbe1;--add-text:#116329;--del-bg:#ffebe9;--del-text:#82071e;--accent:#0969da;--purple:#8250df;--orange:#9a6700;--teal:#1a7f37;color-scheme:light}

[data-theme="dark"]{--bg:#0d1117;--surface:#161b22;--border:#30363d;--text:#e6edf3;--text-muted:#8b949e;--add-bg:#12261e;--add-text:#3fb950;--del-bg:#2d1214;--del-text:#f85149;--accent:#58a6ff;--purple:#bc8cff;--orange:#d29922;--teal:#39d353;color-scheme:dark}

@media(prefers-color-scheme:dark){:root:not([data-theme="light"]){--bg:#0d1117;--surface:#161b22;--border:#30363d;--text:#e6edf3;--text-muted:#8b949e;--add-bg:#12261e;--add-text:#3fb950;--del-bg:#2d1214;--del-text:#f85149;--accent:#58a6ff;--purple:#bc8cff;--orange:#d29922;--teal:#39d353;color-scheme:dark}}

body{margin:0;background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'Segoe
UI',Helvetica,Arial,sans-serif}
.container{max-width:1200px;margin:0 auto;padding:2rem 1rem}

.theme-toggle{position:fixed;top:1rem;right:1rem;z-index:100;background:var(--surface);border:1px
solid var(--border);border-radius:8px;padding:.4rem
.6rem;cursor:pointer;font-size:1.1rem;line-height:1;color:var(--text)}
.theme-toggle:hover{border-color:var(--accent)}
.pr-header{background:var(--surface);border:1px solid
var(--border);border-radius:10px;padding:1.25rem
1.5rem;margin-bottom:1.5rem}
.pr-header h1{margin:0 0 .5rem;font-size:1.35rem;font-weight:600}
.pr-header h1 span{color:var(--text-muted);font-weight:400}

.pr-meta{display:flex;flex-wrap:wrap;gap:.75rem;align-items:center;font-size:.82rem;color:var(--text-muted)}
.pr-meta .badge{background:var(--bg);border:1px solid
var(--border);border-radius:6px;padding:.15rem
.5rem;font-family:'SFMono-Regular',Consolas,monospace;font-size:.75rem}
.pr-meta .stat-add{color:var(--add-text);font-weight:600}
.pr-meta .stat-del{color:var(--del-text);font-weight:600}

.summary-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1rem;margin-bottom:1.5rem}
.summary-card{background:var(--surface);border:1px solid
var(--border);border-radius:8px;padding:1rem 1.25rem}
.summary-card
.label{font-size:.7rem;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted);margin-bottom:.25rem}
.summary-card .value{font-size:1.6rem;font-weight:700;color:var(--text)}
.summary-card
.desc{font-size:.78rem;color:var(--text-muted);margin-top:.25rem}
.summary{border:1px solid var(--teal);border-radius:8px;padding:1rem
1.25rem;margin-bottom:1.5rem}
.summary h2{margin:0 0
.5rem;font-size:1rem;color:var(--teal);display:flex;align-items:center;gap:.5rem}
.summary p{margin:0;font-size:.88rem;line-height:1.6;color:var(--text)}
.finding{background:var(--surface);border:1px solid
var(--border);border-radius:8px;margin-bottom:.75rem;overflow:hidden}

.finding-header{display:flex;align-items:center;gap:.75rem;padding:.75rem
1rem;cursor:pointer;user-select:none}
.finding-header:hover{background:rgba(110,118,129,.06)}
.finding-header h3{margin:0;font-size:.9rem;font-weight:600;flex:1}
.finding-body{padding:.5rem 1rem
1rem;font-size:.85rem;line-height:1.65;color:var(--text)}
.finding.collapsed .finding-body{display:none}
.severity{display:inline-block;padding:.15rem
.6rem;border-radius:12px;font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;flex-shrink:0}

.severity.positive{background:rgba(57,211,83,.15);color:var(--teal);border:1px
solid rgba(57,211,83,.3)}

.severity.info{background:rgba(88,166,255,.15);color:var(--accent);border:1px
solid rgba(88,166,255,.3)}

.severity.low{background:rgba(210,153,34,.15);color:var(--orange);border:1px
solid rgba(210,153,34,.3)}

.severity.medium{background:rgba(248,81,73,.15);color:var(--del-text);border:1px
solid rgba(248,81,73,.3)}
.previously{background:rgba(188,140,255,.08);border:1px solid
rgba(188,140,255,.2);border-radius:6px;padding:.4rem .75rem;margin:.4rem
0;font-size:.82rem;color:var(--text-muted);line-height:1.5}
.previously
.label{font-weight:600;color:var(--purple);font-size:.72rem;text-transform:uppercase;letter-spacing:.04em;margin-right:.35rem}
.previously
code{font-family:'SFMono-Regular',Consolas,monospace;font-size:.78rem;background:rgba(110,118,129,.15);padding:.1rem
.3rem;border-radius:3px}

.cross-ref{font-size:.78rem;color:var(--purple);font-style:italic;margin:.25rem
0}
.cross-ref a{color:var(--purple);text-decoration:underline}
.cross-ref a:hover{color:var(--accent)}
.commit-progression{background:var(--surface);border:1px solid
var(--border);border-radius:8px;padding:1rem 1.25rem;margin:1rem 0}
.commit-progression h3{margin:0 0
.5rem;font-size:.9rem;color:var(--text)}
.commit-progression ol{margin:0;padding-left:1.25rem}
.commit-progression
li{margin-bottom:.35rem;font-size:.82rem;color:var(--text-muted);line-height:1.5}
.commit-progression
.commit-type{font-weight:600;font-size:.72rem;text-transform:uppercase;letter-spacing:.04em;margin-right:.35rem;padding:.1rem
.4rem;border-radius:4px;background:rgba(110,118,129,.1);color:var(--text-muted)}

.file-link{color:var(--accent);text-decoration:none;font-family:'SFMono-Regular',Consolas,monospace;font-size:.82rem}
.file-link:hover{text-decoration:underline}
.diff-file{background:var(--surface);border:1px solid
var(--border);border-radius:8px;margin-bottom:.75rem;overflow:hidden}

.diff-file-header{display:flex;align-items:center;gap:.75rem;padding:.6rem
1rem;cursor:pointer;user-select:none;font-size:.82rem}
.diff-file-header:hover{background:rgba(110,118,129,.06)}

.diff-file-path{flex:1;font-family:'SFMono-Regular',Consolas,monospace;font-size:.82rem}
.diff-file-path a{color:var(--text);text-decoration:none}
.diff-file-path a:hover{color:var(--accent);text-decoration:underline}
.diff-file-stats{font-size:.75rem;color:var(--text-muted)}
.diff-body{overflow-x:auto}
.diff-file.collapsed .chevron{transform:rotate(-90deg)}
.diff-file.collapsed .diff-body{display:none}
.chevron{transition:transform
.15s;font-size:.7rem;color:var(--text-muted)}

.diff-table{width:100%;border-collapse:collapse;font-family:'SFMono-Regular',Consolas,monospace;font-size:.78rem;line-height:1.55}
.diff-table td{padding:0 .75rem;white-space:pre;vertical-align:top}
.diff-table
.ln{width:1px;text-align:right;color:var(--text-muted);opacity:.4;padding:0
.5rem;user-select:none}
.diff-table .ln a{color:inherit;text-decoration:none}
.diff-table .ln a:hover{color:var(--accent);text-decoration:underline}
.diff-table tr.add{background:var(--add-bg)}
.diff-table tr.add td.code{color:var(--add-text)}
.diff-table tr.del{background:var(--del-bg)}
.diff-table tr.del td.code{color:var(--del-text)}
.diff-table tr.hunk td{color:var(--purple);padding:.3rem
.75rem;background:rgba(188,140,255,.08);font-style:italic}
.annotation{display:block;background:var(--surface);border:1px solid
var(--border);border-left:3px solid;border-radius:6px;padding:.5rem
.75rem;margin:.35rem
0;font-family:-apple-system,BlinkMacSystemFont,'Segoe
UI',Helvetica,Arial,sans-serif;font-size:.8rem;line-height:1.5;white-space:normal;color:var(--text-muted)}
.annotation.positive{border-left-color:var(--teal)}
.annotation.info{border-left-color:var(--accent)}
.annotation.low{border-left-color:var(--orange)}
.annotation.medium{border-left-color:var(--del-text)}
.diff-controls{display:flex;gap:.5rem;margin-bottom:.75rem}
.diff-controls button{background:var(--surface);border:1px solid
var(--border);border-radius:6px;padding:.3rem
.75rem;cursor:pointer;font-size:.78rem;color:var(--text-muted)}
.diff-controls
button:hover{border-color:var(--accent);color:var(--accent)}
.file-list{background:var(--surface);border:1px solid
var(--border);border-radius:8px;padding:1rem 1.25rem;margin-top:1.5rem}
.file-list h3{margin:0 0 .5rem;font-size:.9rem}
.file-pills{display:flex;flex-wrap:wrap;gap:.4rem}
.file-pill{background:var(--bg);border:1px solid
var(--border);border-radius:4px;padding:.15rem
.5rem;font-family:'SFMono-Regular',Consolas,monospace;font-size:.72rem;color:var(--text-muted)}
section{margin-bottom:1.5rem}
section h2{font-size:1.05rem;margin:0 0 .75rem;color:var(--text)}
</style>
</head>
<body>
<button class="theme-toggle" onclick="toggleTheme()" title="Toggle
theme">&#x1f313;</button>
<div class="container">
<div class="pr-header">
<h1>Convert CreateSampleEventButton to functional component
<span>#115830</span></h1>
<div class="pr-meta">
<span>by <strong>ryan953</strong></span>
<span
class="badge">ryan953/ref/convert-create-sample-event-button</span>
<span>&rarr;</span>
<span class="badge">master</span>
<span class="stat-add">+198</span>
<span class="stat-del">-240</span>
<span>4 files</span>
<span>2 commits</span>
</div>
</div>
<div class="summary-grid">
<div class="summary-card">
<div class="label">Net Change</div>
<div class="value">-42</div>
<div class="desc">Lines removed by eliminating class boilerplate and the
imperative polling loop</div>
</div>
<div class="summary-card">
<div class="label">Pattern</div>
<div class="value">Class &rarr; Hooks</div>
<div class="desc">HOCs replaced with useOrganization, useNavigate;
api.requestPromise replaced with TanStack Query</div>
</div>
<div class="summary-card">
<div class="label">Behavior Change</div>
<div class="value">None</div>
<div class="desc">Pure refactor &mdash; identical UI, analytics, and
error handling</div>
</div>
</div>
<div class="summary">
<h2>&#x1f4cb; Summary</h2>
<p>Converts <code>CreateSampleEventButton</code> from a class component
wrapped in <code>withApi</code>/<code>withOrganization</code> HOCs to a
functional component using hooks and TanStack Query. The manual
<code>while</code>-loop polling with <code>api.requestPromise</code> is
replaced by <code>queryClient.fetchQuery</code> with built-in
<code>retry</code>/<code>retryDelay</code>, and the entire
create-and-poll lifecycle lives inside a single
<code>useMutation</code>. No behavior change.</p>
</div>
<section>
<h2>Logical Changes</h2>
<div class="finding">
<div class="finding-header"
onclick="this.parentElement.classList.toggle('collapsed')">
<span class="severity positive">highlight</span>
<h3>Class component converted to function component with hooks</h3>
<span class="chevron">&#x25BC;</span>
</div>
<div class="finding-body">
<p>The entire class body &mdash; lifecycle methods, instance state,
<code>_isMounted</code> guard, and <code>render()</code> &mdash; is
replaced by a single function component using
<code>useOrganization</code>, <code>useNavigate</code>,
<code>useEffect</code>, and <code>useMutation</code>.</p>
<div class="previously"><span class="label">Previously:</span>
<code>class CreateSampleEventButton extends Component&lt;Props,
State&gt;</code> wrapped in <code>withApi(withOrganization(...))</code>.
Manual <code>this.state.creating</code> flag and
<code>this._isMounted</code> guard for unmount safety.</div>
<p><a class="file-link"
href="https://github.com/getsentry/sentry/pull/115830/files#diff-7d4c7a9d9b9d9d9d9d9d9d9d9d9d9d9d"
target="_blank" rel="noopener">createSampleEventButton.tsx</a></p>
</div>
</div>
<div class="finding">
<div class="finding-header"
onclick="this.parentElement.classList.toggle('collapsed')">
<span class="severity positive">highlight</span>
<h3>Polling loop replaced with queryClient.fetchQuery</h3>
<span class="chevron">&#x25BC;</span>
</div>
<div class="finding-body">
<p>The imperative <code>while (true)</code> loop with
<code>setTimeout</code> and <code>api.requestPromise</code> is replaced
by <code>queryClient.fetchQuery</code> with
<code>retry</code>/<code>retryDelay</code> options. This delegates retry
scheduling to TanStack Query, eliminating manual timer management and
the <code>_isMounted</code> guard.</p>
<div class="previously"><span class="label">Previously:</span>
<code>async function latestEventAvailable()</code> &mdash; a standalone
function with a <code>while (true)</code> loop,
<code>window.setTimeout</code>, and <code>api.requestPromise</code>.
Called from <code>createSampleGroup()</code> after the POST
succeeded.</div>
<p><a class="file-link"
href="https://github.com/getsentry/sentry/pull/115830/files#diff-7d4c7a9d9b9d9d9d9d9d9d9d9d9d9d9d"
target="_blank" rel="noopener">createSampleEventButton.tsx:55-97</a></p>
</div>
</div>
<div class="finding">
<div class="finding-header"
onclick="this.parentElement.classList.toggle('collapsed')">
<span class="severity info">info</span>
<h3>useMutation lifecycle callbacks organize side effects</h3>
<span class="chevron">&#x25BC;</span>
</div>
<div class="finding-body">
<p>Analytics, loading indicators, navigation, and error reporting are
moved into <code>onMutate</code>, <code>onSuccess</code>, and
<code>onError</code> callbacks of <code>useMutation</code>. The
<code>isPending</code> flag from the mutation replaces the manual
<code>this.state.creating</code> boolean.</p>
<div class="previously"><span class="label">Previously:</span> Side
effects were scattered across <code>createSampleGroup()</code> method
body with manual <code>setState({creating: true/false})</code>
calls.</div>
<p><a class="file-link"
href="https://github.com/getsentry/sentry/pull/115830/files#diff-7d4c7a9d9b9d9d9d9d9d9d9d9d9d9d9d"
target="_blank"
rel="noopener">createSampleEventButton.tsx:99-167</a></p>
</div>
</div>
<div class="finding">
<div class="finding-header"
onclick="this.parentElement.classList.toggle('collapsed')">
<span class="severity info">info</span>
<h3>Default export changed to named export</h3>
<span class="chevron">&#x25BC;</span>
</div>
<div class="finding-body">
<p>The component switches from <code>export default
withApi(withOrganization(CreateSampleEventButton))</code> to
<code>export function CreateSampleEventButton</code>. Import sites in
<code>waitingForEvents.tsx</code> and <code>firstEventFooter.tsx</code>
are updated accordingly.</p>
<div class="previously"><span class="label">Previously:</span>
<code>export default
withApi(withOrganization(CreateSampleEventButton))</code></div>
<p><a class="file-link"
href="https://github.com/getsentry/sentry/pull/115830/files#diff-waitingForEvents"
target="_blank" rel="noopener">waitingForEvents.tsx</a> &middot; <a
class="file-link"
href="https://github.com/getsentry/sentry/pull/115830/files#diff-firstEventFooter"
target="_blank" rel="noopener">firstEventFooter.tsx</a></p>
</div>
</div>
<div class="finding">
<div class="finding-header"
onclick="this.parentElement.classList.toggle('collapsed')">
<span class="severity info">info</span>
<h3>Tests updated for new async patterns</h3>
<span class="chevron">&#x25BC;</span>
</div>
<div class="finding-body">
<p>Tests no longer use global <code>jest.useFakeTimers()</code>.
Happy-path tests use real timers since <code>fetchQuery</code> resolves
immediately with mocked responses. The retry test scopes fake timers
locally and uses <code>jest.advanceTimersByTimeAsync</code> wrapped in
<code>act()</code> to properly handle the <code>fetchQuery</code> retry
mechanism.</p>
<div class="previously"><span class="label">Previously:</span> Global
<code>jest.useFakeTimers()</code> with <code>jest.runAllTimers()</code>
to advance the imperative <code>setTimeout</code> polling loop.</div>
<p><a class="file-link"
href="https://github.com/getsentry/sentry/pull/115830/files#diff-spec"
target="_blank" rel="noopener">createSampleEventButton.spec.tsx</a></p>
</div>
</div>
</section>
<div class="commit-progression">
<h3>Commit Progression</h3>
<ol>
<li><span class="commit-type">modernization</span> Convert class to
function component with hooks, replace HOCs, introduce useMutation +
useQuery with retry for polling</li>
<li><span class="commit-type">refinement</span> Replace useQuery bridge
pattern with queryClient.fetchQuery inside useMutation, simplify tests
to use real timers where possible</li>
</ol>
</div>
<section>
<h2>Annotated Diff</h2>
<div class="diff-controls">
<button onclick="toggleAll(true)">Expand all</button>
<button onclick="toggleAll(false)">Collapse all</button>
</div>
<div class="diff-file">
<div class="diff-file-header"
onclick="this.parentElement.classList.toggle('collapsed')">
<span class="chevron">&#x25BC;</span>
<span class="diff-file-path"><a
href="https://github.com/getsentry/sentry/pull/115830/files#diff-7d4c7a9d9b9d9d9d9d9d9d9d9d9d9d9d"
target="_blank" rel="noopener">createSampleEventButton.tsx</a></span>
<span class="severity info">modified</span>
<span class="diff-file-stats"><span
style="color:var(--add-text)">+150</span> <span
style="color:var(--del-text)">-192</span></span>
</div>
<div class="diff-body">
<table class="diff-table">
<tr><td colspan="3"><div class="annotation info">Imports: React hooks,
TanStack Query, apiOptions, fetchMutation, useNavigate, useOrganization
replace class-era imports (Component, Client, withApi, withOrganization,
browserHistory)</div></td></tr>
<tr class="del"><td class="ln">1</td><td class="code">import {Component}
from 'react';</td></tr>
<tr class="add"><td class="ln">1</td><td class="code">import {useEffect}
from 'react';</td></tr>
<tr class="add"><td class="ln">3</td><td class="code">import
{useMutation, useQueryClient} from '@tanstack/react-query';</td></tr>
<tr class="del"><td class="ln">12</td><td class="code">import type
{Client} from 'sentry/api';</td></tr>
<tr class="del"><td class="ln">15</td><td class="code">import
{browserHistory} from 'sentry/utils/browserHistory';</td></tr>
<tr class="add"><td class="ln">13</td><td class="code">import
{apiOptions} from 'sentry/utils/api/apiOptions';</td></tr>
<tr class="add"><td class="ln">14</td><td class="code">import
{fetchMutation} from 'sentry/utils/queryClient';</td></tr>
<tr class="add"><td class="ln">16</td><td class="code">import
{useNavigate} from 'sentry/utils/useNavigate';</td></tr>
<tr class="add"><td class="ln">17</td><td class="code">import
{useOrganization} from 'sentry/utils/useOrganization';</td></tr>
<tr><td colspan="3"><div class="annotation positive">Props simplified:
api and organization props removed (now sourced from hooks). Type and
State interfaces removed.</div></td></tr>
<tr class="del"><td class="ln">21</td><td class="code"> api:
Client;</td></tr>
<tr class="del"><td class="ln">22</td><td class="code"> organization:
Organization;</td></tr>
<tr class="del"><td class="ln">32</td><td class="code">type State = {
creating: boolean; };</td></tr>
<tr><td colspan="3"><div class="annotation positive">The standalone
latestEventAvailable function (while-loop with setTimeout) is removed
entirely. Its logic is now inside queryClient.fetchQuery with
retry/retryDelay.</div></td></tr>
<tr class="del"><td class="ln">37</td><td class="code">async function
latestEventAvailable(api, orgSlug, groupID) { ... }</td></tr>
<tr><td colspan="3"><div class="annotation positive">Class body replaced
by function component. useMutation encapsulates the POST + poll
lifecycle. mutationFn calls fetchMutation for POST, then
queryClient.fetchQuery with apiOptions for polling.</div></td></tr>
<tr class="add"><td class="ln">33</td><td class="code">export function
CreateSampleEventButton({ ... }) {</td></tr>
<tr class="add"><td class="ln">40</td><td class="code"> const
queryClient = useQueryClient();</td></tr>
<tr class="add"><td class="ln">41</td><td class="code"> const navigate =
useNavigate();</td></tr>
<tr class="add"><td class="ln">42</td><td class="code"> const
organization = useOrganization();</td></tr>
<tr><td colspan="3"><div class="annotation info">Side effects organized
into useMutation lifecycle: onMutate (analytics + loading indicator),
onSuccess (clear indicators, analytics, navigate), onError (Sentry
capture + error message).</div></td></tr>
<tr><td colspan="3"><div class="annotation info">Render body simplified:
isPending from useMutation replaces this.state.creating. No more manual
prop destructuring to exclude HOC props.</div></td></tr>
<tr class="del"><td class="ln">219</td><td class="code">export default
withApi(withOrganization(CreateSampleEventButton));</td></tr>
</table>
</div>
</div>
<div class="diff-file collapsed">
<div class="diff-file-header"
onclick="this.parentElement.classList.toggle('collapsed')">
<span class="chevron">&#x25BC;</span>
<span class="diff-file-path"><a
href="https://github.com/getsentry/sentry/pull/115830/files#diff-spec"
target="_blank"
rel="noopener">createSampleEventButton.spec.tsx</a></span>
<span class="severity info">modified</span>
<span class="diff-file-stats"><span
style="color:var(--add-text)">+46</span> <span
style="color:var(--del-text)">-46</span></span>
</div>
<div class="diff-body">
<table class="diff-table">
<tr><td colspan="3"><div class="annotation info">Global
jest.useFakeTimers() removed. Happy-path tests work with real timers
since mock responses resolve immediately. Retry test scopes fake timers
locally with jest.useFakeTimers()/jest.useRealTimers().</div></td></tr>
<tr class="del"><td class="ln">10</td><td
class="code">jest.useFakeTimers();</td></tr>
<tr><td colspan="3"><div class="annotation info">Analytics tests now
mock the GET endpoint (fetchQuery fires after POST) and use waitFor
around analytics assertions since the mutation is fully
async.</div></td></tr>
<tr><td colspan="3"><div class="annotation info">Retry test uses act(()
=&gt; jest.advanceTimersByTimeAsync(EVENT_POLL_INTERVAL)) to properly
advance fetchQuery's internal retry timer within React's act
boundary.</div></td></tr>
</table>
</div>
</div>
<div class="diff-file collapsed">
<div class="diff-file-header"
onclick="this.parentElement.classList.toggle('collapsed')">
<span class="chevron">&#x25BC;</span>
<span class="diff-file-path"><a
href="https://github.com/getsentry/sentry/pull/115830/files#diff-waitingForEvents"
target="_blank" rel="noopener">waitingForEvents.tsx</a></span>
<span class="severity low">import fix</span>
<span class="diff-file-stats"><span
style="color:var(--add-text)">+1</span> <span
style="color:var(--del-text)">-1</span></span>
</div>
<div class="diff-body">
<table class="diff-table">
<tr class="del"><td class="ln">13</td><td class="code">import
CreateSampleEventButton from '...createSampleEventButton';</td></tr>
<tr class="add"><td class="ln">13</td><td class="code">import
{CreateSampleEventButton} from '...createSampleEventButton';</td></tr>
</table>
</div>
</div>
<div class="diff-file collapsed">
<div class="diff-file-header"
onclick="this.parentElement.classList.toggle('collapsed')">
<span class="chevron">&#x25BC;</span>
<span class="diff-file-path"><a
href="https://github.com/getsentry/sentry/pull/115830/files#diff-firstEventFooter"
target="_blank" rel="noopener">firstEventFooter.tsx</a></span>
<span class="severity low">import fix</span>
<span class="diff-file-stats"><span
style="color:var(--add-text)">+1</span> <span
style="color:var(--del-text)">-1</span></span>
</div>
<div class="diff-body">
<table class="diff-table">
<tr class="del"><td class="ln">19</td><td class="code">import
CreateSampleEventButton from '...createSampleEventButton';</td></tr>
<tr class="add"><td class="ln">19</td><td class="code">import
{CreateSampleEventButton} from '...createSampleEventButton';</td></tr>
</table>
</div>
</div>
</section>
<div class="file-list">
<h3>Files Changed</h3>
<div class="file-pills">
<span class="file-pill">createSampleEventButton.tsx</span>
<span class="file-pill">createSampleEventButton.spec.tsx</span>
<span class="file-pill">waitingForEvents.tsx</span>
<span class="file-pill">firstEventFooter.tsx</span>
</div>
</div>
</div>
<script>
function
toggleAll(e){document.querySelectorAll('.diff-file').forEach(el=>{e?el.classList.remove('collapsed'):el.classList.add('collapsed')})}
function toggleTheme(){const
r=document.documentElement,c=r.getAttribute('data-theme');if(c==='dark')r.setAttribute('data-theme','light');else
if(c==='light'){r.removeAttribute('data-theme');r.setAttribute('data-theme','dark')}else{r.setAttribute('data-theme',matchMedia('(prefers-color-scheme:dark)').matches?'light':'dark')}}
</script>
</body>
</html>
html-preview:end -->

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants