123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 |
- import {Fragment} from 'react';
- import {assignToActor, assignToUser} from 'sentry/actionCreators/group';
- import {promptsCheck, promptsUpdate} from 'sentry/actionCreators/prompts';
- import {Client} from 'sentry/api';
- import AsyncComponent from 'sentry/components/asyncComponent';
- import {Actor, CodeOwner, Committer, Group, Organization, Project} from 'sentry/types';
- import {Event} from 'sentry/types/event';
- import {trackIntegrationAnalytics} from 'sentry/utils/integrationUtil';
- import {promptIsDismissed} from 'sentry/utils/promptIsDismissed';
- import withApi from 'sentry/utils/withApi';
- import withCommitters from 'sentry/utils/withCommitters';
- import withOrganization from 'sentry/utils/withOrganization';
- import {findMatchedRules, Rules} from './findMatchedRules';
- import {OwnershipRules} from './ownershipRules';
- import {SuggestedAssignees} from './suggestedAssignees';
- type OwnerList = React.ComponentProps<typeof SuggestedAssignees>['owners'];
- type Props = {
- api: Client;
- event: Event;
- group: Group;
- organization: Organization;
- project: Project;
- committers?: Committer[];
- } & AsyncComponent['props'];
- type State = {
- codeowners: CodeOwner[];
- event_owners: {owners: Array<Actor>; rules: Rules};
- isDismissed: boolean;
- } & AsyncComponent['state'];
- class SuggestedOwners extends AsyncComponent<Props, State> {
- getDefaultState() {
- return {
- ...super.getDefaultState(),
- event: {rules: [], owners: []},
- codeowners: [],
- isDismissed: true,
- };
- }
- getEndpoints(): ReturnType<AsyncComponent['getEndpoints']> {
- const {project, organization, event} = this.props;
- const endpoints = [
- [
- 'event_owners',
- `/projects/${organization.slug}/${project.slug}/events/${event.id}/owners/`,
- ],
- ];
- if (organization.features.includes('integrations-codeowners')) {
- endpoints.push([
- `codeowners`,
- `/projects/${organization.slug}/${project.slug}/codeowners/`,
- ]);
- }
- return endpoints as ReturnType<AsyncComponent['getEndpoints']>;
- }
- async onLoadAllEndpointsSuccess() {
- await this.checkCodeOwnersPrompt();
- }
- componentDidUpdate(prevProps: Props) {
- if (this.props.event && prevProps.event) {
- if (this.props.event.id !== prevProps.event.id) {
- // two events, with different IDs
- this.reloadData();
- }
- return;
- }
- if (this.props.event) {
- // going from having no event to having an event
- this.reloadData();
- }
- }
- async checkCodeOwnersPrompt() {
- const {api, organization, project} = this.props;
- this.setState({loading: true});
- // check our prompt backend
- const promptData = await promptsCheck(api, {
- organizationId: organization.id,
- projectId: project.id,
- feature: 'code_owners',
- });
- const isDismissed = promptIsDismissed(promptData, 30);
- this.setState({isDismissed, loading: false}, () => {
- if (!isDismissed) {
- // now record the results
- trackIntegrationAnalytics(
- 'integrations.show_code_owners_prompt',
- {
- view: 'stacktrace_issue_details',
- project_id: project.id,
- organization,
- },
- {startSession: true}
- );
- }
- });
- }
- handleCTAClose = () => {
- const {api, organization, project} = this.props;
- promptsUpdate(api, {
- organizationId: organization.id,
- projectId: project.id,
- feature: 'code_owners',
- status: 'dismissed',
- });
- this.setState({isDismissed: true}, () =>
- trackIntegrationAnalytics('integrations.dismissed_code_owners_prompt', {
- view: 'stacktrace_issue_details',
- project_id: project.id,
- organization,
- })
- );
- };
- /**
- * Combine the commiter and ownership data into a single array, merging
- * users who are both owners based on having commits, and owners matching
- * project ownership rules into one array.
- *
- * The return array will include objects of the format:
- *
- * {
- * actor: <
- * type, # Either user or team
- * SentryTypes.User, # API expanded user object
- * {email, id, name} # Sentry user which is *not* expanded
- * {email, name} # Unidentified user (from commits)
- * {id, name}, # Sentry team (check `type`)
- * >,
- *
- * # One or both of commits and rules will be present
- *
- * commits: [...] # List of commits made by this owner
- * rules: [...] # Project rules matched for this owner
- * }
- */
- getOwnerList() {
- const committers = this.props.committers ?? [];
- const owners = committers.map(commiter => ({
- actor: {...commiter.author, type: 'user' as Actor['type']},
- commits: commiter.commits,
- })) as OwnerList;
- this.state.event_owners.owners.forEach(owner => {
- const normalizedOwner = {
- actor: owner,
- rules: findMatchedRules(this.state.event_owners.rules || [], owner),
- };
- const existingIdx = owners.findIndex(o =>
- committers.length === 0 ? o.actor === owner : o.actor.email === owner.email
- );
- if (existingIdx > -1) {
- owners[existingIdx] = {...normalizedOwner, ...owners[existingIdx]};
- return;
- }
- owners.push(normalizedOwner);
- });
- return owners;
- }
- handleAssign = (actor: Actor) => () => {
- if (actor.id === undefined) {
- return;
- }
- const {event} = this.props;
- if (actor.type === 'user') {
- // TODO(ts): `event` here may not be 100% correct
- // in this case groupID should always exist on event
- // since this is only used in Issue Details
- assignToUser({
- id: event.groupID as string,
- user: actor,
- assignedBy: 'suggested_assignee',
- });
- }
- if (actor.type === 'team') {
- assignToActor({
- id: event.groupID as string,
- actor,
- assignedBy: 'suggested_assignee',
- });
- }
- };
- renderBody() {
- const {organization, project, group} = this.props;
- const {codeowners, isDismissed} = this.state;
- const owners = this.getOwnerList();
- return (
- <Fragment>
- {owners.length > 0 && (
- <SuggestedAssignees owners={owners} onAssign={this.handleAssign} />
- )}
- <OwnershipRules
- issueId={group.id}
- project={project}
- organization={organization}
- codeowners={codeowners}
- isDismissed={isDismissed}
- handleCTAClose={this.handleCTAClose}
- />
- </Fragment>
- );
- }
- }
- export default withApi(withOrganization(withCommitters(SuggestedOwners)));
|