import capitalize from 'lodash/capitalize';
import * as qs from 'query-string';
import {Result} from 'sentry/components/forms/selectAsyncControl';
import {
IconBitbucket,
IconGeneric,
IconGithub,
IconGitlab,
IconJira,
IconVsts,
} from 'sentry/icons';
import {t} from 'sentry/locale';
import HookStore from 'sentry/stores/hookStore';
import {
AppOrProviderOrPlugin,
DocIntegration,
ExternalActorMapping,
ExternalActorMappingOrSuggestion,
Integration,
IntegrationFeature,
IntegrationInstallationStatus,
IntegrationType,
Organization,
PluginWithProjectList,
SentryApp,
SentryAppInstallation,
} from 'sentry/types';
import {Hooks} from 'sentry/types/hooks';
import {
integrationEventMap,
IntegrationEventParameters,
} from 'sentry/utils/analytics/integrationAnalyticsEvents';
import makeAnalyticsFunction from 'sentry/utils/analytics/makeAnalyticsFunction';
const mapIntegrationParams = analyticsParams => {
// Reload expects integration_status even though it's not relevant for non-sentry apps
// Passing in a dummy value of published in those cases
const fullParams = {...analyticsParams};
if (analyticsParams.integration && analyticsParams.integration_type !== 'sentry_app') {
fullParams.integration_status = 'published';
}
return fullParams;
};
export const trackIntegrationAnalytics = makeAnalyticsFunction<
IntegrationEventParameters,
{organization: Organization} // org is required
>(integrationEventMap, {
mapValuesFn: mapIntegrationParams,
});
/**
* In sentry.io the features list supports rendering plan details. If the hook
* is not registered for rendering the features list like this simply show the
* features as a normal list.
*/
const generateFeaturesList = p => (
{p.features.map((f, i) => (
- {f.description}
))}
);
const generateIntegrationFeatures = p =>
p.children({
disabled: false,
disabledReason: null,
ungatedFeatures: p.features,
gatedFeatureGroups: [],
});
const defaultFeatureGateComponents = {
IntegrationFeatures: generateIntegrationFeatures,
IntegrationDirectoryFeatures: generateIntegrationFeatures,
FeatureList: generateFeaturesList,
IntegrationDirectoryFeatureList: generateFeaturesList,
} as ReturnType;
export const getIntegrationFeatureGate = () => {
const defaultHook = () => defaultFeatureGateComponents;
const featureHook = HookStore.get('integrations:feature-gates')[0] || defaultHook;
return featureHook();
};
export const getSentryAppInstallStatus = (install: SentryAppInstallation | undefined) => {
if (install) {
return capitalize(install.status) as IntegrationInstallationStatus;
}
return 'Not Installed';
};
export const getCategories = (features: IntegrationFeature[]): string[] => {
const transform = features.map(({featureGate}) => {
const feature = featureGate
.replace(/integrations/g, '')
.replace(/-/g, ' ')
.trim();
switch (feature) {
case 'actionable notification':
return 'notification action';
case 'issue basic':
case 'issue link':
case 'issue sync':
return 'project management';
case 'commits':
return 'source code management';
case 'chat unfurl':
return 'chat';
default:
return feature;
}
});
return [...new Set(transform)];
};
export const getCategoriesForIntegration = (
integration: AppOrProviderOrPlugin
): string[] => {
if (isSentryApp(integration)) {
return ['internal', 'unpublished'].includes(integration.status)
? [integration.status]
: getCategories(integration.featureData);
}
if (isPlugin(integration)) {
return getCategories(integration.featureDescriptions);
}
if (isDocIntegration(integration)) {
return getCategories(integration.features ?? []);
}
return getCategories(integration.metadata.features);
};
export function isSentryApp(
integration: AppOrProviderOrPlugin
): integration is SentryApp {
return !!(integration as SentryApp).uuid;
}
export function isPlugin(
integration: AppOrProviderOrPlugin
): integration is PluginWithProjectList {
return integration.hasOwnProperty('shortName');
}
export function isDocIntegration(
integration: AppOrProviderOrPlugin
): integration is DocIntegration {
return integration.hasOwnProperty('isDraft');
}
export function isExternalActorMapping(
mapping: ExternalActorMappingOrSuggestion
): mapping is ExternalActorMapping {
return mapping.hasOwnProperty('id');
}
export const getIntegrationType = (
integration: AppOrProviderOrPlugin
): IntegrationType => {
if (isSentryApp(integration)) {
return 'sentry_app';
}
if (isPlugin(integration)) {
return 'plugin';
}
if (isDocIntegration(integration)) {
return 'document';
}
return 'first_party';
};
export const convertIntegrationTypeToSnakeCase = (
type: 'plugin' | 'firstParty' | 'sentryApp' | 'docIntegration'
) => {
switch (type) {
case 'firstParty':
return 'first_party';
case 'sentryApp':
return 'sentry_app';
case 'docIntegration':
return 'document';
default:
return type;
}
};
export const safeGetQsParam = (param: string) => {
try {
const query = qs.parse(window.location.search) || {};
return query[param];
} catch {
return undefined;
}
};
export const getIntegrationIcon = (integrationType?: string, size?: string) => {
const iconSize = size || 'md';
switch (integrationType) {
case 'bitbucket':
return ;
case 'gitlab':
return ;
case 'github':
case 'github_enterprise':
return ;
case 'jira':
case 'jira_server':
return ;
case 'vsts':
return ;
default:
return ;
}
};
// used for project creation and onboarding
// determines what integration maps to what project platform
export const platformToIntegrationMap = {
'node-awslambda': 'aws_lambda',
'python-awslambda': 'aws_lambda',
};
export const isSlackIntegrationUpToDate = (integrations: Integration[]): boolean => {
return integrations.every(
integration =>
integration.provider.key !== 'slack' || integration.scopes?.includes('commands')
);
};
export const getAlertText = (integrations?: Integration[]): string | undefined => {
return isSlackIntegrationUpToDate(integrations || [])
? undefined
: t(
'Update to the latest version of our Slack app to get access to personal and team notifications.'
);
};
/**
* Uses the mapping and baseEndpoint to derive the details for the mappings request.
* @param baseEndpoint Must have a trailing slash, since the id is appended for PUT requests!
* @param mapping The mapping or suggestion being sent to the endpoint
* @returns An object containing the request method (apiMethod), and final endpoint (apiEndpoint)
*/
export const getExternalActorEndpointDetails = (
baseEndpoint: string,
mapping?: ExternalActorMappingOrSuggestion
): {apiEndpoint: string; apiMethod: 'POST' | 'PUT'} => {
const isValidMapping = mapping && isExternalActorMapping(mapping);
return {
apiMethod: isValidMapping ? 'PUT' : 'POST',
apiEndpoint: isValidMapping ? `${baseEndpoint}${mapping.id}/` : baseEndpoint,
};
};
export const sentryNameToOption = ({id, name}): Result => ({
value: id,
label: name,
});