Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 45 additions & 22 deletions static/app/utils/integrations/useAddIntegration.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import {useAddIntegration} from 'sentry/utils/integrations/useAddIntegration';

describe('useAddIntegration', () => {
const provider = GitHubIntegrationProviderFixture();
const legacyProvider = GitHubIntegrationProviderFixture({
key: 'custom_legacy',
slug: 'custom_legacy',
});
const integration = GitHubIntegrationFixture();
let configState: Config;

Expand Down Expand Up @@ -58,7 +62,7 @@ describe('useAddIntegration', () => {

act(() =>
result.current.startFlow({
provider,
provider: legacyProvider,
organization: OrganizationFixture(),
onInstall: jest.fn(),
})
Expand All @@ -76,7 +80,7 @@ describe('useAddIntegration', () => {

act(() =>
result.current.startFlow({
provider,
provider: legacyProvider,
organization: OrganizationFixture(),
onInstall: jest.fn(),
account: 'my-account',
Expand All @@ -96,7 +100,7 @@ describe('useAddIntegration', () => {

act(() =>
result.current.startFlow({
provider,
provider: legacyProvider,
organization: OrganizationFixture(),
onInstall: jest.fn(),
urlParams: {custom_param: 'value'},
Expand All @@ -114,7 +118,7 @@ describe('useAddIntegration', () => {

act(() =>
result.current.startFlow({
provider,
provider: legacyProvider,
organization: OrganizationFixture(),
onInstall,
})
Expand Down Expand Up @@ -142,7 +146,7 @@ describe('useAddIntegration', () => {

act(() =>
result.current.startFlow({
provider,
provider: legacyProvider,
organization: OrganizationFixture(),
onInstall: jest.fn(),
})
Expand All @@ -159,7 +163,7 @@ describe('useAddIntegration', () => {

act(() =>
result.current.startFlow({
provider,
provider: legacyProvider,
organization: OrganizationFixture(),
onInstall: jest.fn(),
})
Expand All @@ -176,7 +180,7 @@ describe('useAddIntegration', () => {

act(() =>
result.current.startFlow({
provider,
provider: legacyProvider,
organization: OrganizationFixture(),
onInstall: jest.fn(),
})
Expand All @@ -195,7 +199,7 @@ describe('useAddIntegration', () => {

act(() =>
result.current.startFlow({
provider,
provider: legacyProvider,
organization: OrganizationFixture(),
onInstall,
})
Expand All @@ -212,6 +216,7 @@ describe('useAddIntegration', () => {
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 50));
});

expect(onInstall).not.toHaveBeenCalled();
});

Expand All @@ -222,7 +227,7 @@ describe('useAddIntegration', () => {

act(() =>
result.current.startFlow({
provider,
provider: legacyProvider,
organization: OrganizationFixture(),
onInstall,
})
Expand All @@ -241,7 +246,7 @@ describe('useAddIntegration', () => {

act(() =>
result.current.startFlow({
provider,
provider: legacyProvider,
organization: OrganizationFixture(),
onInstall: jest.fn(),
})
Expand All @@ -253,13 +258,11 @@ describe('useAddIntegration', () => {
});

describe('API pipeline flow', () => {
it('opens the pipeline modal when feature flag is enabled', () => {
it('opens the pipeline modal for unconditionally API-driven providers', () => {
const openPipelineModalSpy = jest.spyOn(pipelineModal, 'openPipelineModal');
const onInstall = jest.fn();

const organization = OrganizationFixture({
features: ['integration-api-pipeline-github'],
});
const organization = OrganizationFixture({features: []});

const {result} = renderHookWithProviders(() => useAddIntegration());

Expand All @@ -282,9 +285,7 @@ describe('useAddIntegration', () => {
it('passes urlParams as initialData to the pipeline modal', () => {
const openPipelineModalSpy = jest.spyOn(pipelineModal, 'openPipelineModal');

const organization = OrganizationFixture({
features: ['integration-api-pipeline-github'],
});
const organization = OrganizationFixture({features: []});

const {result} = renderHookWithProviders(() => useAddIntegration());

Expand All @@ -308,9 +309,7 @@ describe('useAddIntegration', () => {
jest.spyOn(pipelineModal, 'openPipelineModal');
jest.spyOn(window, 'open');

const organization = OrganizationFixture({
features: ['integration-api-pipeline-github'],
});
const organization = OrganizationFixture({features: []});

const {result} = renderHookWithProviders(() => useAddIntegration());

Expand All @@ -325,7 +324,31 @@ describe('useAddIntegration', () => {
expect(window.open).not.toHaveBeenCalled();
});

it('falls back to legacy flow when feature flag is not enabled', () => {
it('opens the pipeline modal for other unconditional providers without a flag', () => {
const openPipelineModalSpy = jest.spyOn(pipelineModal, 'openPipelineModal');
const organization = OrganizationFixture({features: []});
const gitlabProvider = GitHubIntegrationProviderFixture({
key: 'gitlab',
slug: 'gitlab',
name: 'GitLab',
});

const {result} = renderHookWithProviders(() => useAddIntegration());

act(() =>
result.current.startFlow({
provider: gitlabProvider,
organization,
onInstall: jest.fn(),
})
);

expect(openPipelineModalSpy).toHaveBeenCalledWith(
expect.objectContaining({provider: 'gitlab'})
);
});
Comment thread
cursor[bot] marked this conversation as resolved.

it('falls back to legacy flow when the provider is not API driven', () => {
const openPipelineModalSpy = jest.spyOn(pipelineModal, 'openPipelineModal');
jest
.spyOn(window, 'open')
Expand All @@ -337,7 +360,7 @@ describe('useAddIntegration', () => {

act(() =>
result.current.startFlow({
provider,
provider: legacyProvider,
organization,
onInstall: jest.fn(),
})
Expand Down
60 changes: 38 additions & 22 deletions static/app/utils/integrations/useAddIntegration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,44 +36,60 @@ export interface AddIntegrationParams {
}

/**
* Per-provider feature flags that gate the new API-driven pipeline setup flow.
* When enabled for a provider, the integration setup uses the React pipeline
* modal instead of the legacy Django view popup window.
* Providers that should always use the API-driven pipeline modal.
*/
const UNCONDITIONAL_API_PIPELINE_PROVIDERS = [
'aws_lambda',
'bitbucket',
'github',
'gitlab',
'slack',
] as const satisfies ReadonlyArray<ProvidersByType['integration']>;

type UnconditionalApiPipelineProvider =
(typeof UNCONDITIONAL_API_PIPELINE_PROVIDERS)[number];

/**
* Providers that support the API-driven pipeline modal but still require an
* organization feature flag during rollout.
*
* Keys are provider identifiers (constrained to registered pipeline providers
* via `satisfies`), values are feature flag names without the `organizations:`
* prefix.
* Keys are provider identifiers, values are feature flag names without the
* `organizations:` prefix.
*/
const API_PIPELINE_FEATURE_FLAGS = {
aws_lambda: 'integration-api-pipeline-aws-lambda',
bitbucket: 'integration-api-pipeline-bitbucket',
github: 'integration-api-pipeline-github',
gitlab: 'integration-api-pipeline-gitlab',
slack: 'integration-api-pipeline-slack',
} as const satisfies Partial<Record<ProvidersByType['integration'], string>>;
const API_PIPELINE_FEATURE_FLAGS = {} as const satisfies Partial<
Record<ProvidersByType['integration'], string>
>;

type ApiPipelineProvider = keyof typeof API_PIPELINE_FEATURE_FLAGS;
type FlaggedApiPipelineProvider = keyof typeof API_PIPELINE_FEATURE_FLAGS;
type ApiPipelineProvider = UnconditionalApiPipelineProvider | FlaggedApiPipelineProvider;

function getApiPipelineProvider(
organization: Organization,
providerKey: string
): ApiPipelineProvider | null {
if (!(providerKey in API_PIPELINE_FEATURE_FLAGS)) {
return null;
if (
UNCONDITIONAL_API_PIPELINE_PROVIDERS.includes(
providerKey as UnconditionalApiPipelineProvider
)
) {
return providerKey as UnconditionalApiPipelineProvider;
}
const key = providerKey as ApiPipelineProvider;
const flag = API_PIPELINE_FEATURE_FLAGS[key];
if (!organization.features.includes(flag)) {
return null;

if (providerKey in API_PIPELINE_FEATURE_FLAGS) {
const key = providerKey as keyof typeof API_PIPELINE_FEATURE_FLAGS;
if (organization.features.includes(API_PIPELINE_FEATURE_FLAGS[key])) {
return key;
}
}
return key;

return null;
}

/**
* Opens the integration setup flow. Accepts all parameters at call time via
* `startFlow(params)`, so a single hook instance can launch flows for any
* provider. Automatically selects between the API-driven pipeline modal and
* the legacy popup-based flow depending on the organization's feature flags.
* the legacy popup-based flow based on the provider's rollout state.
*
* The hook manages its own `message` event listener for the legacy popup flow.
* No context provider is needed.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {GitHubIntegrationProviderFixture} from 'sentry-fixture/githubIntegrationProvider';
import {IntegrationProviderFixture} from 'sentry-fixture/integrationProvider';
import {OrganizationFixture} from 'sentry-fixture/organization';
import {ProjectFixture} from 'sentry-fixture/project';

Expand All @@ -12,7 +12,7 @@ jest.mock('sentry/actionCreators/modal');
describe('AddIntegrationRow', () => {
let org: any;
const project = ProjectFixture();
const provider = GitHubIntegrationProviderFixture();
const provider = IntegrationProviderFixture();

beforeEach(() => {
org = OrganizationFixture();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/* global global */
import {GitHubIntegrationProviderFixture} from 'sentry-fixture/githubIntegrationProvider';
import {IntegrationProviderFixture} from 'sentry-fixture/integrationProvider';
import {OrganizationFixture} from 'sentry-fixture/organization';

import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';

import {AddIntegrationButton} from 'sentry/views/settings/organizationIntegrations/addIntegrationButton';

describe('AddIntegrationButton', () => {
const provider = GitHubIntegrationProviderFixture();
const provider = IntegrationProviderFixture();

it('Opens the setup dialog on click', async () => {
const focus = jest.fn();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {GitHubIntegrationProviderFixture} from 'sentry-fixture/githubIntegrationProvider';
import {IntegrationProviderFixture} from 'sentry-fixture/integrationProvider';
import {OrganizationFixture} from 'sentry-fixture/organization';
import {ProjectFixture} from 'sentry-fixture/project';

Expand All @@ -17,7 +17,7 @@ describe('AddIntegrationButton', () => {
const project = ProjectFixture();

beforeEach(() => {
provider = GitHubIntegrationProviderFixture();
provider = IntegrationProviderFixture();
org = OrganizationFixture();
hasAccess = true;
externalInstallText = undefined;
Expand Down
Loading
Loading