Skip to content

Commit 9566165

Browse files
alexsohn1126claude
andcommitted
feat(slack): Nudge per-workspace to update outdated Slack app installs
Detect an outdated Slack app installation by the absence of the app_mentions:read scope, which is only granted to current installs. Previously the check keyed on the older 'commands' scope, which no longer distinguishes outdated installs. On the integration configurations tab, compute the upgrade prompt per-workspace so that an outdated Slack workspace no longer flags a sibling workspace that is already current. Also refresh the nudge copy to reflect the new capability (tag Sentry to triage and debug issues) and render the directory 'Update Now' button as a primary action. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent cf0d909 commit 9566165

4 files changed

Lines changed: 66 additions & 13 deletions

File tree

static/app/utils/integrationUtil.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -282,18 +282,27 @@ export function getCodeOwnerIcon(
282282
return <IconSentry size={iconSize} />;
283283
}
284284
}
285+
const isIntegrationUpToDate = (integration: Integration): boolean =>
286+
integration.provider.key !== 'slack' ||
287+
(integration.scopes?.includes('app_mentions:read') ?? false);
288+
285289
const isSlackIntegrationUpToDate = (integrations: Integration[]): boolean => {
286-
return integrations.every(
287-
integration =>
288-
integration.provider.key !== 'slack' || integration.scopes?.includes('commands')
289-
);
290+
return integrations.every(isIntegrationUpToDate);
290291
};
291292

293+
/**
294+
* Whether a single integration installation is running an outdated app and
295+
* should surface an "Update Now" prompt. Checked per-workspace so that, e.g.,
296+
* an outdated Slack workspace doesn't flag a sibling workspace that is current.
297+
*/
298+
export const integrationRequiresUpgrade = (integration: Integration): boolean =>
299+
!isIntegrationUpToDate(integration);
300+
292301
export const getAlertText = (integrations?: Integration[]): string | undefined => {
293302
return isSlackIntegrationUpToDate(integrations || [])
294303
? undefined
295304
: t(
296-
'Update to the latest version of our Slack app to get access to personal and team notifications.'
305+
'Update to the latest version of our Slack app to tag Sentry and ask it to triage and debug issues'
297306
);
298307
};
299308

static/app/views/settings/organizationIntegrations/integrationDetailedView.spec.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,53 @@ describe('IntegrationDetailedView', () => {
165165
expect(screen.getByRole('button', {name: 'Configure'})).toBeEnabled();
166166
});
167167

168+
it('shows Update Now only for the outdated Slack workspace', async () => {
169+
const slackProvider = {
170+
aspects: {},
171+
canAdd: true,
172+
canDisable: false,
173+
features: ['alert-rule', 'chat-unfurl'],
174+
key: 'slack',
175+
name: 'Slack',
176+
slug: 'slack',
177+
};
178+
MockApiClient.addMockResponse({
179+
url: `/organizations/${organization.slug}/integrations/`,
180+
match: [MockApiClient.matchQuery({provider_key: 'slack', includeConfig: 0})],
181+
body: [
182+
{
183+
id: '10',
184+
name: 'Outdated Workspace',
185+
domainName: 'outdated.slack.com',
186+
provider: slackProvider,
187+
status: 'active',
188+
// Missing app_mentions:read -> outdated install.
189+
scopes: ['commands', 'chat:write'],
190+
},
191+
{
192+
id: '11',
193+
name: 'Current Workspace',
194+
domainName: 'current.slack.com',
195+
provider: slackProvider,
196+
status: 'active',
197+
scopes: ['commands', 'chat:write', 'app_mentions:read'],
198+
},
199+
],
200+
});
201+
202+
render(<IntegrationDetailedView />, {
203+
initialRouterConfig: createRouterConfig('slack', {tab: 'configurations'}),
204+
organization,
205+
});
206+
207+
expect(await screen.findByText('Outdated Workspace')).toBeInTheDocument();
208+
expect(screen.getByText('Current Workspace')).toBeInTheDocument();
209+
210+
// Only the outdated workspace surfaces an Update Now button, not every row.
211+
expect(screen.getByTestId('integration-upgrade-button')).toBeInTheDocument();
212+
expect(screen.getAllByTestId('integration-upgrade-button')).toHaveLength(1);
213+
});
214+
168215
it('disables configure for members without access', async () => {
169216
const lowerAccessOrg = OrganizationFixture({access: ['org:read']});
170217
render(<IntegrationDetailedView />, {

static/app/views/settings/organizationIntegrations/integrationDetailedView.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {getApiUrl} from 'sentry/utils/api/getApiUrl';
2525
import {
2626
getAlertText,
2727
getIntegrationStatus,
28+
integrationRequiresUpgrade,
2829
isScmProvider,
2930
trackIntegrationAnalytics,
3031
} from 'sentry/utils/integrationUtil';
@@ -406,7 +407,7 @@ export default function IntegrationDetailedView() {
406407
organization,
407408
});
408409
}}
409-
requiresUpgrade={!!alertText}
410+
requiresUpgrade={integrationRequiresUpgrade(integration)}
410411
/>
411412
</PanelItem>
412413
))}

static/app/views/settings/organizationIntegrations/integrationRow.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,9 @@ export function IntegrationRow(props: Props) {
118118
<Alert
119119
variant="warning"
120120
trailingItems={
121-
<ResolveNowButton
121+
<LinkButton
122122
href={`${baseUrl}?tab=configurations&referrer=directory_resolve_now`}
123+
variant="primary"
123124
size="xs"
124125
onClick={() =>
125126
trackIntegrationAnalytics('integrations.resolve_now_clicked', {
@@ -130,7 +131,7 @@ export function IntegrationRow(props: Props) {
130131
}
131132
>
132133
{resolveText || t('Resolve Now')}
133-
</ResolveNowButton>
134+
</LinkButton>
134135
}
135136
>
136137
{alertText}
@@ -195,8 +196,3 @@ const PublishStatus = styled(({status, ...props}: PublishStatusProps) => (
195196
font-weight: ${p => p.theme.font.weight.sans.regular};
196197
}
197198
`;
198-
199-
const ResolveNowButton = styled(LinkButton)`
200-
color: ${p => p.theme.tokens.content.secondary};
201-
float: right;
202-
`;

0 commit comments

Comments
 (0)