Skip to content

Commit

Permalink
Switch from working with "Team: Foo" labels to "Product Area: Bar" (#436
Browse files Browse the repository at this point in the history
)
  • Loading branch information
chadwhitacre authored May 1, 2023
1 parent 03ff0c8 commit c38b89d
Show file tree
Hide file tree
Showing 16 changed files with 372 additions and 333 deletions.
11 changes: 11 additions & 0 deletions migrations/20230426174813_teams-to-product-areas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Knex } from 'knex';

export async function up(knex: Knex): Promise<void> {
// Since we only have ~a dozen subscriptions here (no good visibility,
// actually, since we don't have direct db access in prod and no web or Slack
// UI set up), we decided to start over from scratch for the new product
// area-based mapping.
await knex('label_to_channel').del();
}

export async function down(knex: Knex): Promise<void> {}
2 changes: 1 addition & 1 deletion src/brain/githubMetrics/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe('github webhook', function () {
beforeAll(async () => {
await db.migrate.latest();
await getLabelsTable().insert({
label_name: 'Team: Test',
label_name: 'Product Area: Test',
channel_id: 'CHNLIDRND1',
offices: ['sfo'],
});
Expand Down
2 changes: 1 addition & 1 deletion src/brain/issueLabelHandler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Time to Triage

We track Time to Triage [as an SLO][looker] for the Open Source Team and the
We track Time to Triage [as an SLO][looker] for the OSPO and the
EPD org as a whole. The computation is [defined in LookML][implementation]
based on the `Status: Untriaged` label. The handler in this directory
implements logic to manipulate the `Status: Untriaged` label in repos that are
Expand Down
59 changes: 32 additions & 27 deletions src/brain/issueLabelHandler/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('issueLabelHandler', function () {
.spyOn(businessHourFunctions, 'calculateSLOViolationTriage')
.mockReturnValue('2022-12-21T00:00:00.000Z');
await getLabelsTable().insert({
label_name: 'Team: Test',
label_name: 'Product Area: Test',
channel_id: 'CHNLIDRND1',
offices: ['sfo'],
});
Expand Down Expand Up @@ -294,9 +294,9 @@ describe('issueLabelHandler', function () {
expect(octokit.issues._comments).toEqual([]);
});

it('removes unrouted label when team label is added', async function () {
it('removes unrouted label when product area label is added', async function () {
await createIssue('sentry-docs');
await addLabel('Team: Test', 'sentry-docs');
await addLabel('Product Area: Test', 'sentry-docs');
expectUntriaged();
expectRouted();
expect(octokit.issues._comments).toEqual([
Expand All @@ -305,7 +305,7 @@ describe('issueLabelHandler', function () {
]);
});

it('does not remove unrouted label when label is added that is not a team label', async function () {
it('does not remove unrouted label when label is added that is not a product area label', async function () {
await createIssue('sentry-docs');
await addLabel('Status: Needs More Information', 'sentry-docs');
expectUnrouted();
Expand All @@ -314,35 +314,35 @@ describe('issueLabelHandler', function () {
]);
});

it('should try to use label description if team label name does not exist', async function () {
it('should try to use label description if product area label name does not exist', async function () {
await createIssue('sentry-docs');
await addLabel('Team: Does Not Exist', 'sentry-docs', 'test');
await addLabel('Product Area: Does Not Exist', 'sentry-docs', 'test');
expectUntriaged();
expect(octokit.issues._comments).toEqual([
'Assigning to @getsentry/support for [routing](https://open.sentry.io/triage/#2-route), due by **<time datetime=2022-12-20T00:00:00.000Z>Monday, December 19th at 4:00 pm</time> (sfo)**. ⏲️',
'Routing to @getsentry/test for [triage](https://develop.sentry.dev/processing-tickets/#3-triage), due by **<time datetime=2022-12-21T00:00:00.000Z>Tuesday, December 20th at 4:00 pm</time> (sfo)**. ⏲️',
]);
});

it('should default to route to open source team if team does not exist', async function () {
it('should default to route to open source team if product area does not exist', async function () {
await createIssue('sentry-docs');
await addLabel('Team: Does Not Exist', 'sentry-docs');
await addLabel('Product Area: Does Not Exist', 'sentry-docs');
expectUntriaged();
expectRouted();
expect(octokit.issues._comments).toEqual([
'Assigning to @getsentry/support for [routing](https://open.sentry.io/triage/#2-route), due by **<time datetime=2022-12-20T00:00:00.000Z>Monday, December 19th at 4:00 pm</time> (sfo)**. ⏲️',
'Failed to route to Team: Does Not Exist. Defaulting to @getsentry/open-source for [triage](https://develop.sentry.dev/processing-tickets/#3-triage), due by **<time datetime=2022-12-21T00:00:00.000Z>Tuesday, December 20th at 4:00 pm</time> (sfo)**. ⏲️',
'Failed to route to Product Area: Does Not Exist. Defaulting to @getsentry/open-source for [triage](https://develop.sentry.dev/processing-tickets/#3-triage), due by **<time datetime=2022-12-21T00:00:00.000Z>Tuesday, December 20th at 4:00 pm</time> (sfo)**. ⏲️',
]);
});

it('removes previous Team labels when re[routing](https://open.sentry.io/triage/#2-route)', async function () {
it('removes previous Product Area labels when re[routing](https://open.sentry.io/triage/#2-route)', async function () {
await createIssue('sentry-docs');
await addLabel('Team: Test', 'sentry-docs');
await addLabel('Product Area: Test', 'sentry-docs');
expectUntriaged();
expectRouted();
await addLabel('Team: Rerouted', 'sentry-docs');
expect(octokit.issues._labels).toContain('Team: Rerouted');
expect(octokit.issues._labels).not.toContain('Team: Test');
await addLabel('Product Area: Rerouted', 'sentry-docs');
expect(octokit.issues._labels).toContain('Product Area: Rerouted');
expect(octokit.issues._labels).not.toContain('Product Area: Test');
expect(octokit.issues._comments).toEqual([
'Assigning to @getsentry/support for [routing](https://open.sentry.io/triage/#2-route), due by **<time datetime=2022-12-20T00:00:00.000Z>Monday, December 19th at 4:00 pm</time> (sfo)**. ⏲️',
'Routing to @getsentry/test for [triage](https://develop.sentry.dev/processing-tickets/#3-triage), due by **<time datetime=2022-12-21T00:00:00.000Z>Tuesday, December 20th at 4:00 pm</time> (sfo)**. ⏲️',
Expand All @@ -352,13 +352,13 @@ describe('issueLabelHandler', function () {

it('should not reroute if Status: Backlog is exists on issue', async function () {
await createIssue('sentry-docs');
await addLabel('Team: Test', 'sentry-docs');
await addLabel('Product Area: Test', 'sentry-docs');
expectUntriaged();
expectRouted();
await addLabel('Status: Backlog', 'sentry-docs');
await addLabel('Team: Rerouted', 'sentry-docs');
expect(octokit.issues._labels).toContain('Team: Rerouted');
expect(octokit.issues._labels).toContain('Team: Test');
await addLabel('Product Area: Rerouted', 'sentry-docs');
expect(octokit.issues._labels).toContain('Product Area: Rerouted');
expect(octokit.issues._labels).toContain('Product Area: Test');
expect(octokit.issues._comments).toEqual([
'Assigning to @getsentry/support for [routing](https://open.sentry.io/triage/#2-route), due by **<time datetime=2022-12-20T00:00:00.000Z>Monday, December 19th at 4:00 pm</time> (sfo)**. ⏲️',
'Routing to @getsentry/test for [triage](https://develop.sentry.dev/processing-tickets/#3-triage), due by **<time datetime=2022-12-21T00:00:00.000Z>Tuesday, December 20th at 4:00 pm</time> (sfo)**. ⏲️',
Expand All @@ -367,13 +367,13 @@ describe('issueLabelHandler', function () {

it('should not reroute if Status: In Progress exists on issue', async function () {
await createIssue('sentry-docs');
await addLabel('Team: Test', 'sentry-docs');
await addLabel('Product Area: Test', 'sentry-docs');
expectUntriaged();
expectRouted();
await addLabel('Status: In Progress', 'sentry-docs');
await addLabel('Team: Rerouted', 'sentry-docs');
expect(octokit.issues._labels).toContain('Team: Rerouted');
expect(octokit.issues._labels).toContain('Team: Test');
await addLabel('Product Area: Rerouted', 'sentry-docs');
expect(octokit.issues._labels).toContain('Product Area: Rerouted');
expect(octokit.issues._labels).toContain('Product Area: Test');
expect(octokit.issues._comments).toEqual([
'Assigning to @getsentry/support for [routing](https://open.sentry.io/triage/#2-route), due by **<time datetime=2022-12-20T00:00:00.000Z>Monday, December 19th at 4:00 pm</time> (sfo)**. ⏲️',
'Routing to @getsentry/test for [triage](https://develop.sentry.dev/processing-tickets/#3-triage), due by **<time datetime=2022-12-21T00:00:00.000Z>Tuesday, December 20th at 4:00 pm</time> (sfo)**. ⏲️',
Expand All @@ -382,12 +382,17 @@ describe('issueLabelHandler', function () {

it('should not reroute if issue is closed', async function () {
await createIssue('sentry-docs');
await addLabel('Team: Test', 'sentry-docs');
await addLabel('Product Area: Test', 'sentry-docs');
expectUntriaged();
expectRouted();
await addLabel('Team: Rerouted', 'sentry-docs', undefined, 'closed');
expect(octokit.issues._labels).toContain('Team: Rerouted');
expect(octokit.issues._labels).toContain('Team: Test');
await addLabel(
'Product Area: Rerouted',
'sentry-docs',
undefined,
'closed'
);
expect(octokit.issues._labels).toContain('Product Area: Rerouted');
expect(octokit.issues._labels).toContain('Product Area: Test');
expect(octokit.issues._comments).toEqual([
'Assigning to @getsentry/support for [routing](https://open.sentry.io/triage/#2-route), due by **<time datetime=2022-12-20T00:00:00.000Z>Monday, December 19th at 4:00 pm</time> (sfo)**. ⏲️',
'Routing to @getsentry/test for [triage](https://develop.sentry.dev/processing-tickets/#3-triage), due by **<time datetime=2022-12-21T00:00:00.000Z>Tuesday, December 20th at 4:00 pm</time> (sfo)**. ⏲️',
Expand All @@ -412,7 +417,7 @@ describe('issueLabelHandler', function () {
jest
.spyOn(businessHourFunctions, 'calculateSLOViolationTriage')
.mockReturnValue('2022-12-21T13:00:00.000Z');
await addLabel('Team: Test', 'sentry-docs');
await addLabel('Product Area: Test', 'sentry-docs');
expectUntriaged();
expectRouted();
expect(octokit.issues._comments).toEqual([
Expand Down
58 changes: 34 additions & 24 deletions src/brain/issueLabelHandler/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import moment from 'moment-timezone';
import {
OFFICE_TIME_ZONES,
OFFICES_24_HOUR,
PRODUCT_AREA_LABEL_PREFIX,
SENTRY_ORG,
TEAM_LABEL_PREFIX,
} from '@/config';
import {
calculateSLOViolationRoute,
Expand All @@ -17,6 +17,7 @@ import {
import { getOssUserType } from '@utils/getOssUserType';
import { isFromABot } from '@utils/isFromABot';

const LENGTH_OF_PRODUCT_AREA_LABEL_PREFIX = 14;
const REPOS_TO_TRACK_FOR_ROUTING = new Set(['sentry', 'sentry-docs']);

import { ClientType } from '@/api/github/clientType';
Expand Down Expand Up @@ -54,7 +55,7 @@ function isNotInARepoWeCareAboutForRouting(payload) {

function isValidLabel(payload) {
return (
!payload.label?.name.startsWith(TEAM_LABEL_PREFIX) ||
!payload.label?.name.startsWith(PRODUCT_AREA_LABEL_PREFIX) ||
payload.issue.labels?.some(
(label) =>
label.name === 'Status: Backlog' || label.name === 'Status: In Progress'
Expand All @@ -65,7 +66,7 @@ function isValidLabel(payload) {

function shouldLabelBeRemoved(label, target_name) {
return (
(label.name.startsWith('Team: ') && label.name !== target_name) ||
(label.name.startsWith('Product Area: ') && label.name !== target_name) ||
(label.name.startsWith('Status: ') && label.name !== UNTRIAGED_LABEL)
);
}
Expand Down Expand Up @@ -103,7 +104,7 @@ export async function markUnrouted({

const timeToRouteBy = await calculateSLOViolationRoute(UNROUTED_LABEL);
const { readableDueByDate, lastOfficeInBusinessHours } =
await getReadableTimeStamp(timeToRouteBy, 'Team: Support');
await getReadableTimeStamp(timeToRouteBy, 'Product Area: Unknown');
await octokit.issues.createComment({
owner,
repo: payload.repository.name,
Expand All @@ -114,10 +115,17 @@ export async function markUnrouted({
tx.finish();
}

async function routeIssue(octokit, teamLabelName, teamDescription) {
async function routeIssue(
octokit,
productAreaLabelName,
productAreaLabelDescription
) {
try {
const strippedTeamName =
teamLabelName?.substr(6).replace(' ', '-').toLowerCase() || '';
productAreaLabelName
?.substr(LENGTH_OF_PRODUCT_AREA_LABEL_PREFIX)
.replace(' ', '-')
.toLowerCase() || '';
await octokit.teams.getByName({
org: SENTRY_ORG,
team_slug: strippedTeamName,
Expand All @@ -126,32 +134,34 @@ async function routeIssue(octokit, teamLabelName, teamDescription) {
} catch (error) {
// If the label name doesn't work, try description
try {
const descriptionSlugName = teamDescription || '';
const descriptionSlugName = productAreaLabelDescription || '';
await octokit.teams.getByName({
org: SENTRY_ORG,
team_slug: descriptionSlugName,
});
return `Routing to @${SENTRY_ORG}/${descriptionSlugName} for [triage](https://develop.sentry.dev/processing-tickets/#3-triage)`;
} catch (error) {
Sentry.captureException(error);
return `Failed to route to ${teamLabelName}. Defaulting to @${SENTRY_ORG}/open-source for [triage](https://develop.sentry.dev/processing-tickets/#3-triage)`;
return `Failed to route to ${productAreaLabelName}. Defaulting to @${SENTRY_ORG}/open-source for [triage](https://develop.sentry.dev/processing-tickets/#3-triage)`;
}
}
}

async function getReadableTimeStamp(timeToTriageBy, teamLabelName) {
async function getReadableTimeStamp(timeToTriageBy, productAreaLabelName) {
const dueByMoment = moment(timeToTriageBy).utc();
const officesForTeam = await getSortedOffices(teamLabelName);
const officesForProductArea = await getSortedOffices(productAreaLabelName);
let lastOfficeInBusinessHours;
(officesForTeam.length > 0 ? officesForTeam : ['sfo']).forEach((office) => {
if (isTimeInBusinessHours(dueByMoment, office)) {
lastOfficeInBusinessHours = office;
(officesForProductArea.length > 0 ? officesForProductArea : ['sfo']).forEach(
(office) => {
if (isTimeInBusinessHours(dueByMoment, office)) {
lastOfficeInBusinessHours = office;
}
}
});
);
if (lastOfficeInBusinessHours == null) {
lastOfficeInBusinessHours = 'sfo';
Sentry.captureMessage(
`Unable to find an office in business hours for ${teamLabelName} for time ${timeToTriageBy}`
`Unable to find an office in business hours for ${productAreaLabelName} for time ${timeToTriageBy}`
);
}
const officeDateFormat =
Expand Down Expand Up @@ -187,16 +197,16 @@ export async function markRouted({
}

const { issue, label } = payload;
const teamLabel = label;
const teamLabelName = teamLabel?.name;
const productAreaLabel = label;
const productAreaLabelName = productAreaLabel?.name;
// Remove Unrouted label when routed.
const owner = payload.repository.owner.login;
const octokit = await getClient(ClientType.App, owner);
const labelsToRemove: string[] = [];

// When routing, remove all Status and Team labels that currently exist on issue
// When routing, remove all Status and Product Area labels that currently exist on issue
issue.labels?.forEach((label) => {
if (shouldLabelBeRemoved(label, teamLabelName)) {
if (shouldLabelBeRemoved(label, productAreaLabelName)) {
labelsToRemove.push(label.name);
}
});
Expand Down Expand Up @@ -230,19 +240,19 @@ export async function markRouted({
labels: [UNTRIAGED_LABEL],
});

const teamLabelDescription = teamLabel?.description;
const productAreaLabelDescription = productAreaLabel?.description;
const routedTeam = await routeIssue(
octokit,
teamLabelName,
teamLabelDescription
productAreaLabelName,
productAreaLabelDescription
);

const timeToTriageBy = await calculateSLOViolationTriage(UNTRIAGED_LABEL, [
teamLabel,
productAreaLabel,
]);

const { readableDueByDate, lastOfficeInBusinessHours } =
await getReadableTimeStamp(timeToTriageBy, teamLabelName);
await getReadableTimeStamp(timeToTriageBy, productAreaLabelName);
const dueBy = `due by **<time datetime=${timeToTriageBy}>${readableDueByDate}</time> (${lastOfficeInBusinessHours})**. ⏲️`;
const comment = `${routedTeam}, ${dueBy}`;
await octokit.issues.createComment({
Expand Down
11 changes: 6 additions & 5 deletions src/brain/issueNotifier/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# issueNotifier

Notifies team channels when they have a new issue [pending triage](https://open.sentry.io/triage/#3-triage). This also notifies the support
channel when a new issue comes in that is unrouted.
Notifies product owner channels when they have a new issue [pending
triage](https://open.sentry.io/triage/#3-triage). This also notifies the
support channel when a new issue comes in that is unrouted.

This requires adding the `/notify-for-triage` command to the bot config.

- `/notify-for-triage`: List all team label subscriptions
- `/notify-for-triage <name> <office>`: Subscribe to all untriaged issues for `Team: <name>` label in an office location (sfo, sea, yyz, vie, ams).
- `/notify-for-triage -<name> <office>`: Unsubscribe from untriaged issues for `Team: <name>` label in an office location (sfo, sea, yyz, vie, ams).
- `/notify-for-triage`: List all product area label subscriptions
- `/notify-for-triage <name> <office>`: Subscribe to all untriaged issues for `Product Area: <name>` label in an office location (sfo, sea, yyz, vie, ams).
- `/notify-for-triage -<name> <office>`: Unsubscribe from untriaged issues for `Product Area: <name>` label in an office location (sfo, sea, yyz, vie, ams).
Loading

0 comments on commit c38b89d

Please sign in to comment.