ref(onboarding): Convert CreateSampleEventButton to functional component#115830
Merged
Conversation
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>
Contributor
📊 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>
Contributor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ 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.
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>
scttcper
approved these changes
May 20, 2026
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. [](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">🌓</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>→</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 → 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 — identical UI, analytics, and error handling</div> </div> </div> <div class="summary"> <h2>📋 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">▼</span> </div> <div class="finding-body"> <p>The entire class body — lifecycle methods, instance state, <code>_isMounted</code> guard, and <code>render()</code> — 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<Props, State></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">▼</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> — 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">▼</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">▼</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> · <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">▼</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">▼</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">▼</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(() => 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">▼</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">▼</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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Convert
CreateSampleEventButtonfrom a class component withwithApi/withOrganizationHOCs to a functional component using hooks. No behavior change.API modernization
Replace
api.requestPromisewith TanStack Query primitives:useMutation+fetchMutationfor the POST, andqueryClient.fetchQuerywithapiOptions,retry, andretryDelayfor polling the latest event. The manualwhile-loop withsetTimeoutis removed entirely. ReplacebrowserHistory.pushwithuseNavigate. 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.fetchQuerypolls the latest-event endpoint using built-inretry/retryDelayoptions instead of an imperative loop. This eliminates the_isMountedguard —useMutationhandles unmount cleanup automatically.