diff --git a/migrations/20230426174813_teams-to-product-areas.ts b/migrations/20230426174813_teams-to-product-areas.ts new file mode 100644 index 00000000..a424e58f --- /dev/null +++ b/migrations/20230426174813_teams-to-product-areas.ts @@ -0,0 +1,11 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + // 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 {} diff --git a/src/brain/githubMetrics/index.test.ts b/src/brain/githubMetrics/index.test.ts index 5f36e71d..3df640a5 100644 --- a/src/brain/githubMetrics/index.test.ts +++ b/src/brain/githubMetrics/index.test.ts @@ -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'], }); diff --git a/src/brain/issueLabelHandler/README.md b/src/brain/issueLabelHandler/README.md index 2016559b..9bbbef57 100644 --- a/src/brain/issueLabelHandler/README.md +++ b/src/brain/issueLabelHandler/README.md @@ -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 diff --git a/src/brain/issueLabelHandler/index.test.ts b/src/brain/issueLabelHandler/index.test.ts index daf557f7..354ebc1f 100644 --- a/src/brain/issueLabelHandler/index.test.ts +++ b/src/brain/issueLabelHandler/index.test.ts @@ -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'], }); @@ -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([ @@ -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(); @@ -314,9 +314,9 @@ 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 ** (sfo)**. ⏲️', @@ -324,25 +324,25 @@ describe('issueLabelHandler', function () { ]); }); - 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 ** (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 ** (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 ** (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 ** (sfo)**. ⏲️', 'Routing to @getsentry/test for [triage](https://develop.sentry.dev/processing-tickets/#3-triage), due by ** (sfo)**. ⏲️', @@ -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 ** (sfo)**. ⏲️', 'Routing to @getsentry/test for [triage](https://develop.sentry.dev/processing-tickets/#3-triage), due by ** (sfo)**. ⏲️', @@ -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 ** (sfo)**. ⏲️', 'Routing to @getsentry/test for [triage](https://develop.sentry.dev/processing-tickets/#3-triage), due by ** (sfo)**. ⏲️', @@ -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 ** (sfo)**. ⏲️', 'Routing to @getsentry/test for [triage](https://develop.sentry.dev/processing-tickets/#3-triage), due by ** (sfo)**. ⏲️', @@ -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([ diff --git a/src/brain/issueLabelHandler/route.ts b/src/brain/issueLabelHandler/route.ts index c6c3d93b..49b34474 100644 --- a/src/brain/issueLabelHandler/route.ts +++ b/src/brain/issueLabelHandler/route.ts @@ -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, @@ -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'; @@ -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' @@ -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) ); } @@ -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, @@ -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, @@ -126,7 +134,7 @@ 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, @@ -134,24 +142,26 @@ async function routeIssue(octokit, teamLabelName, teamDescription) { 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 = @@ -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); } }); @@ -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 ** (${lastOfficeInBusinessHours})**. ⏲️`; const comment = `${routedTeam}, ${dueBy}`; await octokit.issues.createComment({ diff --git a/src/brain/issueNotifier/README.md b/src/brain/issueNotifier/README.md index 8f17f2bb..890b503a 100644 --- a/src/brain/issueNotifier/README.md +++ b/src/brain/issueNotifier/README.md @@ -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 `: Subscribe to all untriaged issues for `Team: ` label in an office location (sfo, sea, yyz, vie, ams). -- `/notify-for-triage - `: Unsubscribe from untriaged issues for `Team: ` label in an office location (sfo, sea, yyz, vie, ams). +- `/notify-for-triage`: List all product area label subscriptions +- `/notify-for-triage `: Subscribe to all untriaged issues for `Product Area: ` label in an office location (sfo, sea, yyz, vie, ams). +- `/notify-for-triage - `: Unsubscribe from untriaged issues for `Product Area: ` label in an office location (sfo, sea, yyz, vie, ams). diff --git a/src/brain/issueNotifier/index.test.ts b/src/brain/issueNotifier/index.test.ts index ed7600b2..ce5e0a63 100644 --- a/src/brain/issueNotifier/index.test.ts +++ b/src/brain/issueNotifier/index.test.ts @@ -15,7 +15,7 @@ describe('issueNotifier Tests', function () { await db.migrate.latest(); for (let i = 1; i <= NUM_CHANNELS; i++) { await getLabelsTable().insert({ - label_name: 'Team: Test', + label_name: 'Product Area: Test', channel_id: channelId(i), offices: null, }); @@ -38,14 +38,14 @@ describe('issueNotifier Tests', function () { false, ], [ - 'Only team label', - { label: { name: 'Team: Test', id: 'test-id' } }, + 'Only product area label', + { label: { name: 'Product Area: Test', id: 'test-id' } }, false, ], [ - `Team label on ${UNTRIAGED_LABEL}`, + `Product Area label on ${UNTRIAGED_LABEL}`, { - label: { name: 'Team: Test', id: 'test-id1' }, + label: { name: 'Product Area: Test', id: 'test-id1' }, issue: { labels: [{ name: UNTRIAGED_LABEL, id: 'test-id2' }] }, }, true, @@ -56,21 +56,21 @@ describe('issueNotifier Tests', function () { false, ], [ - `${UNTRIAGED_LABEL} on Team label`, + `${UNTRIAGED_LABEL} on Product Area label`, { label: { name: UNTRIAGED_LABEL, id: 'test-id1' }, - issue: { labels: [{ name: 'Team: Test', id: 'test-id2' }] }, + issue: { labels: [{ name: 'Product Area: Test', id: 'test-id2' }] }, }, true, ], [ - `Random label on Team + ${UNTRIAGED_LABEL}`, + `Random label on Product Area + ${UNTRIAGED_LABEL}`, { label: { name: 'Random Label', id: 'random' }, issue: { labels: [ { name: UNTRIAGED_LABEL, id: 'test-id1' }, - { name: 'Team: Test', id: 'test-id2' }, + { name: 'Product Area: Test', id: 'test-id2' }, ], }, }, @@ -104,7 +104,7 @@ describe('issueNotifier Tests', function () { it('should escape issue titles with < or > characters', async function () { const payload = { - label: { name: 'Team: Test', id: 'random' }, + label: { name: 'Product Area: Test', id: 'random' }, issue: { labels: [{ name: UNTRIAGED_LABEL, id: 'test-id2' }] }, }; const eventPayload = hydrateGitHubEventAndPayload('issues', { @@ -182,7 +182,7 @@ describe('issueNotifier Tests', function () { ack = jest.fn(); }); - it('should respond that channel is not subscribed to any team notifications if channel does not exist', async function () { + it('should respond that channel is not subscribed to any product area notifications if channel does not exist', async function () { const channel_id = channelId(3); const command = { channel_id, @@ -190,12 +190,12 @@ describe('issueNotifier Tests', function () { }; await slackHandler({ command, ack, say, respond, client }); expect(say).lastCalledWith( - 'This channel is not subscribed to any team notifications.' + 'This channel is not subscribed to any product area notifications.' ); expect(await getLabelsTable().where({ channel_id })).toEqual([]); }); - it('should respond that channel is subscribed to team test if office is null', async function () { + it('should respond that channel is subscribed to product area test if office is null', async function () { const channel_id = channelId(1); const command = { channel_id, @@ -203,12 +203,12 @@ describe('issueNotifier Tests', function () { }; await slackHandler({ command, ack, say, respond, client }); expect(say).lastCalledWith( - 'This channel is set to receive notifications for: Team: Test (no office specified)' + 'This channel is set to receive notifications for: Product Area: Test (no office specified)' ); expect(await getLabelsTable().where({ channel_id })).toMatchObject([ { channel_id: 'CHNLIDRND1', - label_name: 'Team: Test', + label_name: 'Product Area: Test', offices: null, }, ]); @@ -222,18 +222,18 @@ describe('issueNotifier Tests', function () { }; await slackHandler({ command, ack, say, respond, client }); expect(say).lastCalledWith( - 'Add office location sfo on the current channel (test) for Team: Test' + 'Add office location sfo on the current channel (test) for Product Area: Test' ); expect(await getLabelsTable().where({ channel_id })).toMatchObject([ { channel_id: 'CHNLIDRND1', - label_name: 'Team: Test', + label_name: 'Product Area: Test', offices: ['sfo'], }, ]); }); - it('should respond that channel is subscribed to team test if office is sfo', async function () { + it('should respond that channel is subscribed to product area test if office is sfo', async function () { const channel_id = channelId(1); const command = { channel_id, @@ -241,12 +241,12 @@ describe('issueNotifier Tests', function () { }; await slackHandler({ command, ack, say, respond, client }); expect(say).lastCalledWith( - 'This channel is set to receive notifications for: Team: Test (sfo)' + 'This channel is set to receive notifications for: Product Area: Test (sfo)' ); expect(await getLabelsTable().where({ channel_id })).toMatchObject([ { channel_id: 'CHNLIDRND1', - label_name: 'Team: Test', + label_name: 'Product Area: Test', offices: ['sfo'], }, ]); @@ -260,12 +260,12 @@ describe('issueNotifier Tests', function () { }; await slackHandler({ command, ack, say, respond, client }); expect(say).lastCalledWith( - 'Add office location sea on the current channel (test) for Team: Test' + 'Add office location sea on the current channel (test) for Product Area: Test' ); expect(await getLabelsTable().where({ channel_id })).toMatchObject([ { channel_id: 'CHNLIDRND1', - label_name: 'Team: Test', + label_name: 'Product Area: Test', offices: ['sfo', 'sea'], }, ]); @@ -279,12 +279,12 @@ describe('issueNotifier Tests', function () { }; await slackHandler({ command, ack, say, respond, client }); expect(say).lastCalledWith( - 'This channel is set to receive notifications for: Team: Test (sfo, sea)' + 'This channel is set to receive notifications for: Product Area: Test (sfo, sea)' ); expect(await getLabelsTable().where({ channel_id })).toMatchObject([ { channel_id: 'CHNLIDRND1', - label_name: 'Team: Test', + label_name: 'Product Area: Test', offices: ['sfo', 'sea'], }, ]); @@ -298,12 +298,12 @@ describe('issueNotifier Tests', function () { }; await slackHandler({ command, ack, say, respond, client }); expect(say).lastCalledWith( - 'Add office location vie on the current channel (test) for Team: Test' + 'Add office location vie on the current channel (test) for Product Area: Test' ); expect(await getLabelsTable().where({ channel_id })).toMatchObject([ { channel_id: 'CHNLIDRND1', - label_name: 'Team: Test', + label_name: 'Product Area: Test', offices: ['sfo', 'sea', 'vie'], }, ]); @@ -317,18 +317,18 @@ describe('issueNotifier Tests', function () { }; await slackHandler({ command, ack, say, respond, client }); expect(say).lastCalledWith( - 'Add office location yyz on the current channel (test) for Team: Test' + 'Add office location yyz on the current channel (test) for Product Area: Test' ); expect(await getLabelsTable().where({ channel_id })).toMatchObject([ { channel_id: 'CHNLIDRND1', - label_name: 'Team: Test', + label_name: 'Product Area: Test', offices: ['sfo', 'sea', 'vie', 'yyz'], }, ]); }); - it('should delete notifications for Team test for office sea', async function () { + it('should delete notifications for Product Area test for office sea', async function () { const channel_id = channelId(1); const command = { channel_id, @@ -336,18 +336,18 @@ describe('issueNotifier Tests', function () { }; await slackHandler({ command, ack, say, respond, client }); expect(say).lastCalledWith( - 'This channel (test) will no longer get notifications for Team: Test during sea business hours.' + 'This channel (test) will no longer get notifications for Product Area: Test during sea business hours.' ); expect(await getLabelsTable().where({ channel_id })).toMatchObject([ { channel_id: 'CHNLIDRND1', - label_name: 'Team: Test', + label_name: 'Product Area: Test', offices: ['sfo', 'vie', 'yyz'], }, ]); }); - it('should not delete notifications for Team test if office is not included', async function () { + it('should not delete notifications for Product Area test if office is not included', async function () { const channel_id = channelId(1); const command = { channel_id, @@ -355,18 +355,18 @@ describe('issueNotifier Tests', function () { }; await slackHandler({ command, ack, say, respond, client }); expect(say).lastCalledWith( - 'This channel (test) is not subscribed to Team: Test during sea business hours.' + 'This channel (test) is not subscribed to Product Area: Test during sea business hours.' ); expect(await getLabelsTable().where({ channel_id })).toMatchObject([ { channel_id: 'CHNLIDRND1', - label_name: 'Team: Test', + label_name: 'Product Area: Test', offices: ['sfo', 'vie', 'yyz'], }, ]); }); - it('should delete notifications for Team test for office yyz', async function () { + it('should delete notifications for Product Area test for office yyz', async function () { const channel_id = channelId(1); const command = { channel_id, @@ -374,18 +374,18 @@ describe('issueNotifier Tests', function () { }; await slackHandler({ command, ack, say, respond, client }); expect(say).lastCalledWith( - 'This channel (test) will no longer get notifications for Team: Test during yyz business hours.' + 'This channel (test) will no longer get notifications for Product Area: Test during yyz business hours.' ); expect(await getLabelsTable().where({ channel_id })).toMatchObject([ { channel_id: 'CHNLIDRND1', - label_name: 'Team: Test', + label_name: 'Product Area: Test', offices: ['sfo', 'vie'], }, ]); }); - it('should delete notifications for Team test for office sfo', async function () { + it('should delete notifications for Product Area test for office sfo', async function () { const channel_id = channelId(1); const command = { channel_id, @@ -393,18 +393,18 @@ describe('issueNotifier Tests', function () { }; await slackHandler({ command, ack, say, respond, client }); expect(say).lastCalledWith( - 'This channel (test) will no longer get notifications for Team: Test during sfo business hours.' + 'This channel (test) will no longer get notifications for Product Area: Test during sfo business hours.' ); expect(await getLabelsTable().where({ channel_id })).toMatchObject([ { channel_id: 'CHNLIDRND1', - label_name: 'Team: Test', + label_name: 'Product Area: Test', offices: ['vie'], }, ]); }); - it('should delete notifications for Team test for office vie', async function () { + it('should delete notifications for Product Area test for office vie', async function () { const channel_id = channelId(1); const command = { channel_id, @@ -412,7 +412,7 @@ describe('issueNotifier Tests', function () { }; await slackHandler({ command, ack, say, respond, client }); expect(say).lastCalledWith( - 'This channel (test) will no longer get notifications for Team: Test during vie business hours.' + 'This channel (test) will no longer get notifications for Product Area: Test during vie business hours.' ); expect(await getLabelsTable().where({ channel_id })).toEqual([]); }); diff --git a/src/brain/issueNotifier/index.ts b/src/brain/issueNotifier/index.ts index 2b4bf2f3..4272191e 100644 --- a/src/brain/issueNotifier/index.ts +++ b/src/brain/issueNotifier/index.ts @@ -1,6 +1,10 @@ import { EmitterWebhookEvent } from '@octokit/webhooks'; -import { TEAM_LABEL_PREFIX, UNROUTED_LABEL, UNTRIAGED_LABEL } from '@/config'; +import { + PRODUCT_AREA_LABEL_PREFIX, + UNROUTED_LABEL, + UNTRIAGED_LABEL, +} from '@/config'; import { githubEvents } from '@api/github'; import { bolt } from '@api/slack'; import { cacheOffices } from '@utils/businessHours'; @@ -18,15 +22,15 @@ export const githubLabelHandler = async ({ return undefined; } - let teamLabel: undefined | string; + let productAreaLabel: undefined | string; if ( - label.name.startsWith(TEAM_LABEL_PREFIX) && + label.name.startsWith(PRODUCT_AREA_LABEL_PREFIX) && issue.labels?.some((label) => label.name === UNTRIAGED_LABEL) ) { - teamLabel = label.name; + productAreaLabel = label.name; } else if (label.name === UNTRIAGED_LABEL) { - teamLabel = issue.labels?.find((label) => - label.name.startsWith(TEAM_LABEL_PREFIX) + productAreaLabel = issue.labels?.find((label) => + label.name.startsWith(PRODUCT_AREA_LABEL_PREFIX) )?.name; } else if (label.name === UNROUTED_LABEL) { bolt.client.chat.postMessage({ @@ -37,7 +41,7 @@ export const githubLabelHandler = async ({ }); } - if (!teamLabel) { + if (!productAreaLabel) { return undefined; } @@ -47,7 +51,7 @@ export const githubLabelHandler = async ({ const channelsToNotify = ( await getLabelsTable() .where({ - label_name: teamLabel, + label_name: productAreaLabel, }) .select('channel_id') ).map((row) => row.channel_id); @@ -66,9 +70,9 @@ export const githubLabelHandler = async ({ ); }; -// /notify-for-triage`: List all team label subscriptions -// /notify-for-triage `: Subscribe to all untriaged issues for `Team: ` label -// /notify-for-triage - `: Unsubscribe from untriaged issues for `Team: ` label +// /notify-for-triage`: List all product area label subscriptions +// /notify-for-triage `: Subscribe to all untriaged issues for `Product Area: ` label +// /notify-for-triage - `: Unsubscribe from untriaged issues for `Product Area: ` label export const slackHandler = async ({ command, ack, say, respond, client }) => { const pending: Promise[] = []; // Acknowledge command request @@ -89,11 +93,11 @@ export const slackHandler = async ({ command, ack, say, respond, client }) => { ? `This channel is set to receive notifications for: ${labels.join( ', ' )}` - : `This channel is not subscribed to any team notifications.`; + : `This channel is not subscribed to any product area notifications.`; pending.push(say(response)); } else { const op = args.op || '+'; - const label_name = `Team: ${args.label}`; + const label_name = `Product Area: ${args.label}`; const newOffice = args.office; const currentOffices = ( @@ -213,7 +217,7 @@ export const slackHandler = async ({ command, ack, say, respond, client }) => { // but leaving when they unsubscribe is not sure game. break; } - // Update cache for the offices mapped to each team + // Update cache for the offices mapped to each product area await cacheOffices(label_name); } await Promise.all(pending); diff --git a/src/buildServer.ts b/src/buildServer.ts index 424c7f0e..56828224 100644 --- a/src/buildServer.ts +++ b/src/buildServer.ts @@ -102,7 +102,7 @@ export async function buildServer( server.post( '/webhooks/pubsub', PubSub.opts, - PubSub.notifyTeamsForUntriagedIssues + PubSub.notifyProductOwnersForUntriagedIssues ); return server; diff --git a/src/config/index.ts b/src/config/index.ts index d0b3d4b5..dcad9ebd 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -85,7 +85,7 @@ export enum BuildStatus { /** * GitHub Triage */ -export const TEAM_LABEL_PREFIX = 'Team: '; +export const PRODUCT_AREA_LABEL_PREFIX = 'Product Area: '; export const UNTRIAGED_LABEL = 'Status: Untriaged'; export const UNROUTED_LABEL = 'Status: Unrouted'; export const MAX_TRIAGE_DAYS = 2; diff --git a/src/utils/businessHours.test.ts b/src/utils/businessHours.test.ts index e19f9efb..c4dc8c19 100644 --- a/src/utils/businessHours.test.ts +++ b/src/utils/businessHours.test.ts @@ -42,22 +42,22 @@ describe('businessHours tests', function () { beforeAll(async function () { await db.migrate.latest(); await getLabelsTable().insert({ - label_name: 'Team: Test', + label_name: 'Product Area: Test', channel_id: 'CHNLIDRND1', offices: ['sfo'], }); await getLabelsTable().insert({ - label_name: 'Team: Undefined', + label_name: 'Product Area: Undefined', channel_id: 'CHNLIDRND1', offices: undefined, }); await getLabelsTable().insert({ - label_name: 'Team: Null', + label_name: 'Product Area: Null', channel_id: 'CHNLIDRND1', offices: null, }); await getLabelsTable().insert({ - label_name: 'Team: Open Source', + label_name: 'Product Area: Other', channel_id: 'CHNLIDRND1', offices: ['sfo'], }); @@ -112,7 +112,7 @@ describe('businessHours tests', function () { it(`should calculate TTT SLO violation for ${testTimestamps[i].day}`, async function () { const result = await calculateTimeToRespondBy( MAX_TRIAGE_DAYS, - 'Team: Test', + 'Product Area: Test', testTimestamps[i].timestamp ); expect(result).toEqual(triageResults[i]); @@ -121,7 +121,7 @@ describe('businessHours tests', function () { it(`should calculate TTR SLO violation for ${testTimestamps[i].day}`, async function () { const result = await calculateTimeToRespondBy( MAX_ROUTE_DAYS, - 'Team: Test', + 'Product Area: Test', testTimestamps[i].timestamp ); expect(result).toEqual(routingResults[i]); @@ -131,7 +131,7 @@ describe('businessHours tests', function () { it('should handle case when offices is undefined', async function () { const result = await calculateTimeToRespondBy( MAX_TRIAGE_DAYS, - 'Team: Undefined', + 'Product Area: Undefined', '2023-12-18T00:00:00.000Z' ); expect(result).toEqual('2023-12-20T01:00:00.000Z'); @@ -140,7 +140,7 @@ describe('businessHours tests', function () { it('should handle case when offices is null', async function () { const result = await calculateTimeToRespondBy( MAX_TRIAGE_DAYS, - 'Team: Null', + 'Product Area: Null', '2023-12-18T00:00:00.000Z' ); expect(result).toEqual('2023-12-20T01:00:00.000Z'); @@ -149,7 +149,7 @@ describe('businessHours tests', function () { it('should handle the last day of the month for TTR', async function () { const result = await calculateTimeToRespondBy( MAX_ROUTE_DAYS, - 'Team: Test', + 'Product Area: Test', '2023-01-31T00:00:00.000Z' ); expect(result).toEqual('2023-02-01T00:00:00.000Z'); @@ -158,7 +158,7 @@ describe('businessHours tests', function () { it('should handle the last day of the year for TTR', async function () { const result = await calculateTimeToRespondBy( MAX_ROUTE_DAYS, - 'Team: Test', + 'Product Area: Test', '2022-12-31T00:00:00.000Z' ); expect(result).toEqual('2023-01-04T01:00:00.000Z'); @@ -168,7 +168,7 @@ describe('businessHours tests', function () { it('should calculate TTT SLO violation for Christmas', async function () { const result = await calculateTimeToRespondBy( MAX_TRIAGE_DAYS, - 'Team: Test', + 'Product Area: Test', '2023-12-24T00:00:00.000Z' ); // 2023-12-24 is Sunday, 2023-12-25/2022-12-26 are holidays @@ -178,7 +178,7 @@ describe('businessHours tests', function () { it('should calculate TTR SLO violation for Christmas', async function () { const result = await calculateTimeToRespondBy( MAX_ROUTE_DAYS, - 'Team: Test', + 'Product Area: Test', '2023-12-24T00:00:00.000Z' ); // 2023-12-24 is Sunday, 2023-12-25/2022-12-26 are holidays @@ -193,7 +193,7 @@ describe('businessHours tests', function () { await slackHandler({ command, ack, say, respond, client }); const result = await calculateTimeToRespondBy( MAX_ROUTE_DAYS, - 'Team: Test', + 'Product Area: Test', '2023-10-02T00:00:00.000Z' ); expect(result).toEqual('2023-10-03T00:00:00.000Z'); @@ -209,7 +209,7 @@ describe('businessHours tests', function () { await slackHandler({ command, ack, say, respond, client }); const result = await calculateTimeToRespondBy( MAX_TRIAGE_DAYS, - 'Team: Test', + 'Product Area: Test', '2023-10-02T00:00:00.000Z' ); expect(result).toEqual('2023-10-03T00:00:00.000Z'); @@ -218,13 +218,13 @@ describe('businessHours tests', function () { it('should calculate weekends properly for friday in sfo, weekend in vie', async function () { const result = await calculateTimeToRespondBy( MAX_TRIAGE_DAYS, - 'Team: Test', + 'Product Area: Test', '2022-12-17T00:00:00.000Z' ); expect(result).toEqual('2022-12-20T00:00:00.000Z'); }); - it('should route properly when team is subscribed to sfo, vie, and yyz', async function () { + it('should route properly when product area is subscribed to sfo, vie, and yyz', async function () { const command = { channel_id: 'CHNLIDRND2', text: 'Test yyz', @@ -232,13 +232,13 @@ describe('businessHours tests', function () { await slackHandler({ command, ack, say, respond, client }); const result = await calculateTimeToRespondBy( MAX_ROUTE_DAYS, - 'Team: Test', + 'Product Area: Test', '2022-12-20T15:30:00.000Z' ); expect(result).toEqual('2022-12-20T23:30:00.000Z'); }); - it('should triage properly when team is subscribed to sfo, vie, and yyz', async function () { + it('should triage properly when product area is subscribed to sfo, vie, and yyz', async function () { const command = { channel_id: 'CHNLIDRND2', text: 'Test yyz', @@ -246,7 +246,7 @@ describe('businessHours tests', function () { await slackHandler({ command, ack, say, respond, client }); const result = await calculateTimeToRespondBy( MAX_TRIAGE_DAYS, - 'Team: Test', + 'Product Area: Test', '2022-12-20T15:30:00.000Z' ); expect(result).toEqual('2022-12-21T14:30:00.000Z'); @@ -278,17 +278,17 @@ describe('businessHours tests', function () { describe('isChannelInBusinessHours', function () { beforeAll(async function () { await getLabelsTable().insert({ - label_name: 'Team: Open Source', + label_name: 'Product Area: Other', channel_id: 'CHNLIDRND4', offices: ['sfo', 'vie'], }); await getLabelsTable().insert({ - label_name: 'Team: Test', + label_name: 'Product Area: Test', channel_id: 'CHNLIDRND4', offices: ['yyz'], }); await getLabelsTable().insert({ - label_name: 'Team: Undefined', + label_name: 'Product Area: Undefined', channel_id: 'CHNLIDRND5', offices: undefined, }); @@ -377,37 +377,38 @@ describe('businessHours tests', function () { describe('calculateSLOViolationTriage', function () { it('should not calculate SLO violation if label is not untriaged', async function () { const result = await calculateSLOViolationTriage('Status: Test', [ - { name: 'Team: Test' }, + { name: 'Product Area: Test' }, ]); expect(result).toEqual(null); }); it('should not calculate SLO violation if label is unrouted', async function () { const result = await calculateSLOViolationTriage(UNROUTED_LABEL, [ - { name: 'Team: Test' }, + { name: 'Product Area: Test' }, ]); expect(result).toEqual(null); }); it('should calculate SLO violation if label is untriaged', async function () { const result = await calculateSLOViolationTriage(UNTRIAGED_LABEL, [ - { name: 'Team: Test' }, + { name: 'Product Area: Test' }, ]); expect(result).not.toEqual(null); }); - it('should calculate SLO violation if label is assigned to another team', async function () { - const result = await calculateSLOViolationTriage('Team: Rerouted', [ - { name: 'Status: Untriaged' }, - ]); + it('should calculate SLO violation if label is assigned to another product area', async function () { + const result = await calculateSLOViolationTriage( + 'Product Area: Rerouted', + [{ name: 'Status: Untriaged' }] + ); expect(result).not.toEqual(null); }); }); describe('getNextAvailableBusinessHourWindow', function () { - it('should get open source team timezones if team does not have offices', async function () { + it('should get open source product area timezones if product area does not have offices', async function () { const { start, end } = await getNextAvailableBusinessHourWindow( - 'Team: Does not exist', + 'Product Area: Does not exist', moment('2022-12-08T12:00:00.000Z').utc() ); expect(start.valueOf()).toEqual( @@ -418,9 +419,9 @@ describe('businessHours tests', function () { ); }); - it('should get sfo timezones for Team: Test', async function () { + it('should get sfo timezones for Product Area: Test', async function () { const { start, end } = await getNextAvailableBusinessHourWindow( - 'Team: Test', + 'Product Area: Test', moment('2022-12-08T12:00:00.000Z').utc() ); expect(start.valueOf()).toEqual( @@ -431,14 +432,14 @@ describe('businessHours tests', function () { ); }); - it('should get vie timezone for Team: Test if it has the closest business hours available', async function () { + it('should get vie timezone for Product Area: Test if it has the closest business hours available', async function () { const command = { channel_id: 'CHNLIDRND2', text: 'Test vie', }; await slackHandler({ command, ack, say, respond, client }); const { start, end } = await getNextAvailableBusinessHourWindow( - 'Team: Test', + 'Product Area: Test', moment('2022-12-08T12:00:00.000Z').utc() ); expect(start.valueOf()).toEqual( @@ -449,14 +450,14 @@ describe('businessHours tests', function () { ); }); - it('should get sfo timezone for Team: Test if it has the closest business hours available', async function () { + it('should get sfo timezone for Product Area: Test if it has the closest business hours available', async function () { const command = { channel_id: 'CHNLIDRND2', text: 'Test vie', }; await slackHandler({ command, ack, say, respond, client }); const { start, end } = await getNextAvailableBusinessHourWindow( - 'Team: Test', + 'Product Area: Test', moment('2022-12-08T16:30:00.000Z').utc() ); expect(start.valueOf()).toEqual( @@ -467,14 +468,14 @@ describe('businessHours tests', function () { ); }); - it('should get yyz timezone for Team: Test if it has the closest business hours available', async function () { + it('should get yyz timezone for Product Area: Test if it has the closest business hours available', async function () { const command = { channel_id: 'CHNLIDRND2', text: 'Test yyz', }; await slackHandler({ command, ack, say, respond, client }); const { start, end } = await getNextAvailableBusinessHourWindow( - 'Team: Test', + 'Product Area: Test', moment('2022-12-08T16:30:00.000Z').utc() ); expect(start.valueOf()).toEqual( @@ -485,9 +486,9 @@ describe('businessHours tests', function () { ); }); - it('should return vie hours for Christmas for team subscribed to vie, yyz, sfo', async function () { + it('should return vie hours for Christmas for product area subscribed to vie, yyz, sfo', async function () { const { start, end } = await getNextAvailableBusinessHourWindow( - 'Team: Test', + 'Product Area: Test', moment('2023-12-23T12:00:00.000Z').utc() ); expect(start.valueOf()).toEqual( @@ -498,9 +499,9 @@ describe('businessHours tests', function () { ); }); - it('should return vie hours for Saturday for team subscribed to vie, yyz, sfo', async function () { + it('should return vie hours for Saturday for product area subscribed to vie, yyz, sfo', async function () { const { start, end } = await getNextAvailableBusinessHourWindow( - 'Team: Test', + 'Product Area: Test', moment('2022-12-17T12:00:00.000Z').utc() ); expect(start.valueOf()).toEqual( @@ -511,9 +512,9 @@ describe('businessHours tests', function () { ); }); - it('should return vie hours for Sunday for team subscribed to vie, yyz, sfo', async function () { + it('should return vie hours for Sunday for product area subscribed to vie, yyz, sfo', async function () { const { start, end } = await getNextAvailableBusinessHourWindow( - 'Team: Test', + 'Product Area: Test', moment('2022-12-18T12:00:00.000Z').utc() ); expect(start.valueOf()).toEqual( @@ -524,14 +525,14 @@ describe('businessHours tests', function () { ); }); - it('should return yyz hours for Saturday for team subscribed to yyz, sfo', async function () { + it('should return yyz hours for Saturday for product area subscribed to yyz, sfo', async function () { let command = { channel_id: 'CHNLIDRND2', text: '-Test vie', }; await slackHandler({ command, ack, say, respond, client }); const { start, end } = await getNextAvailableBusinessHourWindow( - 'Team: Test', + 'Product Area: Test', moment('2022-12-17T12:00:00.000Z').utc() ); expect(start.valueOf()).toEqual( @@ -549,38 +550,38 @@ describe('businessHours tests', function () { }); describe('getOffices', function () { - it('should return empty array if team label is undefined', async function () { + it('should return empty array if product area label is undefined', async function () { expect(await getOffices(undefined)).toEqual([]); }); - it('should return empty array if team offices value is undefined', async function () { - expect(await getOffices('Team: Undefined')).toEqual([]); + it('should return empty array if product area offices value is undefined', async function () { + expect(await getOffices('Product Area: Undefined')).toEqual([]); }); - it('should return empty array if team offices value is null', async function () { - expect(await getOffices('Team: Null')).toEqual([]); + it('should return empty array if product area offices value is null', async function () { + expect(await getOffices('Product Area: Null')).toEqual([]); }); - it('should get sfo office for team test', async function () { - expect(await getOffices('Team: Test')).toEqual(['sfo']); + it('should get sfo office for product area test', async function () { + expect(await getOffices('Product Area: Test')).toEqual(['sfo']); }); - it('should get sfo and vie office for team test if new office is added', async function () { + it('should get sfo and vie office for product area test if new office is added', async function () { const command = { channel_id: 'CHNLIDRND1', text: 'Test vie', }; await slackHandler({ command, ack, say, respond, client }); - expect(await getOffices('Team: Test')).toEqual(['sfo', 'vie']); + expect(await getOffices('Product Area: Test')).toEqual(['sfo', 'vie']); }); - it('should get vie office for team test if existing office is removed', async function () { + it('should get vie office for product area test if existing office is removed', async function () { const command = { channel_id: 'CHNLIDRND1', text: '-Test sfo', }; await slackHandler({ command, ack, say, respond, client }); - expect(await getOffices('Team: Test')).toEqual(['vie']); + expect(await getOffices('Product Area: Test')).toEqual(['vie']); }); it('should get offices from multiple channels', async function () { @@ -589,7 +590,7 @@ describe('businessHours tests', function () { text: 'Test yyz', }; await slackHandler({ command, ack, say, respond, client }); - expect(await getOffices('Team: Test')).toEqual(['vie', 'yyz']); + expect(await getOffices('Product Area: Test')).toEqual(['vie', 'yyz']); command = { channel_id: 'CHNLIDRND2', text: '-Test yyz', @@ -599,22 +600,25 @@ describe('businessHours tests', function () { }); describe('getSortedOffices', function () { - it('should get sfo and vie office in sorted order for team test if new office is added', async function () { + it('should get sfo and vie office in sorted order for product area test if new office is added', async function () { const command = { channel_id: 'CHNLIDRND1', text: 'Test sfo', }; await slackHandler({ command, ack, say, respond, client }); - expect(await getSortedOffices('Team: Test')).toEqual(['vie', 'sfo']); + expect(await getSortedOffices('Product Area: Test')).toEqual([ + 'vie', + 'sfo', + ]); }); - it('should get sfo, vie, yyz offices in sorted order for team test if new office is added', async function () { + it('should get sfo, vie, yyz offices in sorted order for product area test if new office is added', async function () { const command = { channel_id: 'CHNLIDRND1', text: 'Test yyz', }; await slackHandler({ command, ack, say, respond, client }); - expect(await getSortedOffices('Team: Test')).toEqual([ + expect(await getSortedOffices('Product Area: Test')).toEqual([ 'vie', 'yyz', 'sfo', diff --git a/src/utils/businessHours.ts b/src/utils/businessHours.ts index 1705d757..7da501d5 100644 --- a/src/utils/businessHours.ts +++ b/src/utils/businessHours.ts @@ -8,13 +8,13 @@ import { MAX_ROUTE_DAYS, MAX_TRIAGE_DAYS, OFFICE_TIME_ZONES, - TEAM_LABEL_PREFIX, + PRODUCT_AREA_LABEL_PREFIX, UNROUTED_LABEL, UNTRIAGED_LABEL, } from '@/config'; import { bolt } from '@api/slack'; -const OPEN_SOURCE_TEAM_CHANNEL = 'G01F3FQ0T41'; +const OPEN_SOURCE_PRODUCT_AREA_CHANNEL = 'G01F3FQ0T41'; const HOUR_IN_MS = 60 * 60 * 1000; const BUSINESS_DAY_IN_MS = 8 * HOUR_IN_MS; @@ -28,13 +28,17 @@ interface BusinessHourWindow { end: moment.Moment; } -export async function calculateTimeToRespondBy(numDays, team, testTimestamp?) { +export async function calculateTimeToRespondBy( + numDays, + productArea, + testTimestamp? +) { let cursor = testTimestamp !== undefined ? moment(testTimestamp).utc() : moment().utc(); let msRemaining = numDays * BUSINESS_DAY_IN_MS; while (msRemaining > 0) { const nextBusinessHours = await getNextAvailableBusinessHourWindow( - team, + productArea, cursor ); const { start, end }: BusinessHourWindow = nextBusinessHours; @@ -50,14 +54,14 @@ export async function calculateTimeToRespondBy(numDays, team, testTimestamp?) { export async function calculateSLOViolationTriage(target_name, labels) { // calculate time to triage for issues that come in with untriaged label if (target_name === UNTRIAGED_LABEL) { - const team = labels?.find((label) => - label.name.startsWith(TEAM_LABEL_PREFIX) + const productArea = labels?.find((label) => + label.name.startsWith(PRODUCT_AREA_LABEL_PREFIX) )?.name; - return calculateTimeToRespondBy(MAX_TRIAGE_DAYS, team); + return calculateTimeToRespondBy(MAX_TRIAGE_DAYS, productArea); } // calculate time to triage for issues that are rerouted else if ( - target_name.startsWith(TEAM_LABEL_PREFIX) && + target_name.startsWith(PRODUCT_AREA_LABEL_PREFIX) && labels?.some((label) => label.name === UNTRIAGED_LABEL) ) { return calculateTimeToRespondBy(MAX_TRIAGE_DAYS, target_name); @@ -67,18 +71,18 @@ export async function calculateSLOViolationTriage(target_name, labels) { export async function calculateSLOViolationRoute(target_name) { if (target_name === UNROUTED_LABEL) { - return calculateTimeToRespondBy(MAX_ROUTE_DAYS, 'Team: Support'); + return calculateTimeToRespondBy(MAX_ROUTE_DAYS, 'Product Area: Unknown'); } return null; } -export async function cacheOffices(team) { +export async function cacheOffices(productArea) { const offices = [ ...new Set( ( await getLabelsTable() .where({ - label_name: team, + label_name: productArea, }) .select('offices') ) @@ -86,7 +90,7 @@ export async function cacheOffices(team) { .filter((office) => office != null) ), ]; - officesCache[team] = offices; + officesCache[productArea] = offices; return offices; } @@ -135,7 +139,7 @@ export const isChannelInBusinessHours = async ( ).channel?.name; await bolt.client.chat.postMessage({ text: `Hey OSPO, looks like #${channelName} doesn't have offices set.`, - channel: OPEN_SOURCE_TEAM_CHANNEL, + channel: OPEN_SOURCE_PRODUCT_AREA_CHANNEL, }); offices = ['sfo']; } @@ -147,14 +151,14 @@ export const isChannelInBusinessHours = async ( }; export async function getNextAvailableBusinessHourWindow( - team, + productArea, momentTime ): Promise { - let offices = await getOffices(team); + let offices = await getOffices(productArea); if (offices.length === 0) { - offices = await getOffices('Team: Open Source'); + offices = await getOffices('Product Area: Other'); if (offices.length === 0) { - throw new Error('Open Source team not subscribed to any offices.'); + throw new Error('Open Source productArea not subscribed to any offices.'); } } const businessHourWindows: BusinessHourWindow[] = []; @@ -202,19 +206,19 @@ export async function getNextAvailableBusinessHourWindow( return businessHourWindows[0]; } -export async function getOffices(team) { - if (!team) { +export async function getOffices(productArea) { + if (!productArea) { return []; } - if (!officesCache[team]) { - await cacheOffices(team); + if (!officesCache[productArea]) { + await cacheOffices(productArea); } - return officesCache[team]; + return officesCache[productArea]; } -export async function getSortedOffices(team) { +export async function getSortedOffices(productArea) { const timezoneOrder = ['vie', 'ams', 'yyz', 'sfo', 'sea']; - return (await getOffices(team)).sort( + return (await getOffices(productArea)).sort( (a, b) => timezoneOrder.indexOf(a) - timezoneOrder.indexOf(b) ); } diff --git a/src/utils/metrics.test.ts b/src/utils/metrics.test.ts index 3038c142..6a1bc4a4 100644 --- a/src/utils/metrics.test.ts +++ b/src/utils/metrics.test.ts @@ -39,7 +39,7 @@ describe('metrics tests', function () { number: 1234, created_at: null, updated_at: null, - labels: [{ name: 'Team: Test' }], + labels: [{ name: 'Product Area: Test' }], }, label: { id: 1234, @@ -54,12 +54,12 @@ describe('metrics tests', function () { .mockImplementation(() => 1487076708000); await db.migrate.latest(); await getLabelsTable().insert({ - label_name: 'Team: Test', + label_name: 'Product Area: Test', channel_id: 'CHNLIDRND1', offices: ['sfo'], }); await getLabelsTable().insert({ - label_name: 'Team: Open Source', + label_name: 'Product Area: Other', channel_id: 'CHNLIDRND1', offices: ['sfo'], }); diff --git a/src/webhooks/pubsub/README.md b/src/webhooks/pubsub/README.md index ceca95b9..5faf7cca 100644 --- a/src/webhooks/pubsub/README.md +++ b/src/webhooks/pubsub/README.md @@ -13,7 +13,7 @@ type PubSubPayload = { ``` This payload will be sent regularly using the [Cloud Scheduler][cloud_scheduler] -to notify teams about their issues pending triage over [our SLO][process_doc]. +to notify product owners about their issues pending triage over [our SLO][process_doc]. [pubsub]: https://cloud.google.com/run/docs/tutorials/pubsub#integrating-pubsub [cloud_scheduler]: https://cloud.google.com/scheduler/docs/tut-pub-sub#create_a_job diff --git a/src/webhooks/pubsub/index.test.ts b/src/webhooks/pubsub/index.test.ts index b7d073cf..9ba07fb4 100644 --- a/src/webhooks/pubsub/index.test.ts +++ b/src/webhooks/pubsub/index.test.ts @@ -11,17 +11,17 @@ describe('Triage Notification Tests', function () { beforeAll(async function () { await db.migrate.latest(); await getLabelsTable().insert({ - label_name: 'Team: Open Source', + label_name: 'Product Area: Other', channel_id: 'channel1', offices: ['yyz'], }); await getLabelsTable().insert({ - label_name: 'Team: Test', + label_name: 'Product Area: Test', channel_id: 'channel2', offices: ['yyz'], }); await getLabelsTable().insert({ - label_name: 'Team: Open Source', + label_name: 'Product Area: Other', channel_id: 'channel2', offices: ['yyz'], }); @@ -125,41 +125,41 @@ describe('Triage Notification Tests', function () { }); it('should return empty promise if no issues are untriaged', async function () { const notificationChannels = { - channel1: ['Team: Test'], - channel2: ['Team: Test', 'Team: Open Source'], + channel1: ['Product Area: Test'], + channel2: ['Product Area: Test', 'Product Area: Other'], }; - const teamToIssuesMap = { - 'Team: Test': [], - 'Team: Open Source': [], + const productAreaToIssuesMap = { + 'Product Area: Test': [], + 'Product Area: Other': [], }; const now = moment('2022-12-12T17:00:00.000Z'); await Promise.all( - constructSlackMessage(notificationChannels, teamToIssuesMap, now) + constructSlackMessage(notificationChannels, productAreaToIssuesMap, now) ); expect(boltPostMessageSpy).toHaveBeenCalledTimes(0); }); it('should return empty promise if outside business hours', async function () { const notificationChannels = { - channel1: ['Team: Test'], - channel2: ['Team: Test', 'Team: Open Source'], + channel1: ['Product Area: Test'], + channel2: ['Product Area: Test', 'Product Area: Other'], }; - const teamToIssuesMap = { - 'Team: Test': [ + const productAreaToIssuesMap = { + 'Product Area: Test': [ { url: 'https://test.com/issues/1', number: 1, title: 'Test Issue', - teamLabel: 'Team: Test', + productAreaLabel: 'Product Area: Test', triageBy: '2022-12-12T21:00:00.000Z', createdAt: '2022-12-10T21:00:00.000Z', }, ], - 'Team: Open Source': [ + 'Product Area: Other': [ { url: 'https://test.com/issues/2', number: 2, title: 'Open Source Issue', - teamLabel: 'Team: Open Source', + productAreaLabel: 'Product Area: Other', triageBy: '2022-12-12T20:00:00.000Z', createdAt: '2022-12-10T21:00:00.000Z', }, @@ -167,32 +167,32 @@ describe('Triage Notification Tests', function () { }; const now = moment('2022-12-12T00:00:00.000Z'); await Promise.all( - constructSlackMessage(notificationChannels, teamToIssuesMap, now) + constructSlackMessage(notificationChannels, productAreaToIssuesMap, now) ); expect(boltPostMessageSpy).toHaveBeenCalledTimes(0); }); it('should return all issues in overdue if SLA has passed', async function () { const notificationChannels = { - channel1: ['Team: Test'], - channel2: ['Team: Test', 'Team: Open Source'], + channel1: ['Product Area: Test'], + channel2: ['Product Area: Test', 'Product Area: Other'], }; - const teamToIssuesMap = { - 'Team: Test': [ + const productAreaToIssuesMap = { + 'Product Area: Test': [ { url: 'https://test.com/issues/1', number: 1, title: 'Test Issue', - teamLabel: 'Team: Test', + productAreaLabel: 'Product Area: Test', triageBy: '2022-12-12T21:00:00.000Z', createdAt: '2022-12-10T21:00:00.000Z', }, ], - 'Team: Open Source': [ + 'Product Area: Other': [ { url: 'https://test.com/issues/2', number: 2, title: 'Open Source Issue', - teamLabel: 'Team: Open Source', + productAreaLabel: 'Product Area: Other', triageBy: '2022-12-12T20:00:00.000Z', createdAt: '2022-12-10T21:00:00.000Z', }, @@ -201,7 +201,7 @@ describe('Triage Notification Tests', function () { const now = moment('2022-12-12T21:00:00.000Z'); const postMessageSpy = jest.spyOn(bolt.client.chat, 'postMessage'); await Promise.all( - constructSlackMessage(notificationChannels, teamToIssuesMap, now) + constructSlackMessage(notificationChannels, productAreaToIssuesMap, now) ); expect(postMessageSpy).toHaveBeenCalledTimes(2); expect(postMessageSpy).toHaveBeenCalledWith({ @@ -292,15 +292,15 @@ describe('Triage Notification Tests', function () { }); it('should strip issue of < and > characters in slack message', async function () { const notificationChannels = { - channel1: ['Team: Test'], + channel1: ['Product Area: Test'], }; - const teamToIssuesMap = { - 'Team: Test': [ + const productAreaToIssuesMap = { + 'Product Area: Test': [ { url: 'https://test.com/issues/1', number: 1, title: '', - teamLabel: 'Team: Test', + productAreaLabel: 'Product Area: Test', triageBy: '2022-12-12T21:00:00.000Z', createdAt: '2022-12-10T21:00:00.000Z', }, @@ -308,7 +308,7 @@ describe('Triage Notification Tests', function () { url: 'https://test.com/issues/2', number: 2, title: '', - teamLabel: 'Team: Test', + productAreaLabel: 'Product Area: Test', triageBy: '2022-12-12T22:00:00.000Z', createdAt: '2022-12-10T22:00:00.000Z', }, @@ -316,7 +316,7 @@ describe('Triage Notification Tests', function () { url: 'https://test.com/issues/3', number: 3, title: '', - teamLabel: 'Team: Test', + productAreaLabel: 'Product Area: Test', triageBy: '2022-12-14T20:00:00.000Z', createdAt: '2022-12-12T20:00:00.000Z', }, @@ -325,7 +325,7 @@ describe('Triage Notification Tests', function () { const now = moment('2022-12-12T21:00:00.000Z'); const postMessageSpy = jest.spyOn(bolt.client.chat, 'postMessage'); await Promise.all( - constructSlackMessage(notificationChannels, teamToIssuesMap, now) + constructSlackMessage(notificationChannels, productAreaToIssuesMap, now) ); expect(postMessageSpy).toHaveBeenCalledTimes(1); expect(postMessageSpy).toHaveBeenCalledWith({ @@ -407,26 +407,26 @@ describe('Triage Notification Tests', function () { }); it('should always notify if issues are overdue and an hour has passed', async function () { const notificationChannels = { - channel1: ['Team: Test'], - channel2: ['Team: Test', 'Team: Open Source'], + channel1: ['Product Area: Test'], + channel2: ['Product Area: Test', 'Product Area: Other'], }; - const teamToIssuesMap = { - 'Team: Test': [ + const productAreaToIssuesMap = { + 'Product Area: Test': [ { url: 'https://test.com/issues/1', number: 1, title: 'Test Issue', - teamLabel: 'Team: Test', + productAreaLabel: 'Product Area: Test', triageBy: '2022-12-12T21:00:00.000Z', createdAt: '2022-12-10T21:00:00.000Z', }, ], - 'Team: Open Source': [ + 'Product Area: Other': [ { url: 'https://test.com/issues/2', number: 2, title: 'Open Source Issue', - teamLabel: 'Team: Open Source', + productAreaLabel: 'Product Area: Other', triageBy: '2022-12-12T20:00:00.000Z', createdAt: '2022-12-10T21:00:00.000Z', }, @@ -435,13 +435,13 @@ describe('Triage Notification Tests', function () { const now = moment('2022-12-12T21:00:00.000Z'); const postMessageSpy = jest.spyOn(bolt.client.chat, 'postMessage'); await Promise.all( - constructSlackMessage(notificationChannels, teamToIssuesMap, now) + constructSlackMessage(notificationChannels, productAreaToIssuesMap, now) ); expect(postMessageSpy).toHaveBeenCalledTimes(2); await Promise.all( constructSlackMessage( notificationChannels, - teamToIssuesMap, + productAreaToIssuesMap, now.add(1, 'hours') ) ); @@ -449,26 +449,26 @@ describe('Triage Notification Tests', function () { }); it('should return all issues in act fast if SLA is approaching', async function () { const notificationChannels = { - channel1: ['Team: Test'], - channel2: ['Team: Test', 'Team: Open Source'], + channel1: ['Product Area: Test'], + channel2: ['Product Area: Test', 'Product Area: Other'], }; - const teamToIssuesMap = { - 'Team: Test': [ + const productAreaToIssuesMap = { + 'Product Area: Test': [ { url: 'https://test.com/issues/1', number: 1, title: 'Test Issue', - teamLabel: 'Team: Test', + productAreaLabel: 'Product Area: Test', triageBy: '2022-12-12T21:00:00.000Z', createdAt: '2022-12-10T21:00:00.000Z', }, ], - 'Team: Open Source': [ + 'Product Area: Other': [ { url: 'https://test.com/issues/2', number: 2, title: 'Open Source Issue', - teamLabel: 'Team: Open Source', + productAreaLabel: 'Product Area: Other', triageBy: '2022-12-12T20:00:00.000Z', createdAt: '2022-12-10T21:00:00.000Z', }, @@ -477,7 +477,7 @@ describe('Triage Notification Tests', function () { const now = moment('2022-12-12T17:00:00.000Z'); const postMessageSpy = jest.spyOn(bolt.client.chat, 'postMessage'); await Promise.all( - constructSlackMessage(notificationChannels, teamToIssuesMap, now) + constructSlackMessage(notificationChannels, productAreaToIssuesMap, now) ); expect(postMessageSpy).toHaveBeenCalledTimes(2); expect(postMessageSpy).toHaveBeenCalledWith({ @@ -571,26 +571,26 @@ describe('Triage Notification Tests', function () { }); it('should always notify if issue SLA is in the act fast queue on every hour', async function () { const notificationChannels = { - channel1: ['Team: Test'], - channel2: ['Team: Test', 'Team: Open Source'], + channel1: ['Product Area: Test'], + channel2: ['Product Area: Test', 'Product Area: Other'], }; - const teamToIssuesMap = { - 'Team: Test': [ + const productAreaToIssuesMap = { + 'Product Area: Test': [ { url: 'https://test.com/issues/1', number: 1, title: 'Test Issue', - teamLabel: 'Team: Test', + productAreaLabel: 'Product Area: Test', triageBy: '2022-12-12T21:00:00.000Z', createdAt: '2022-12-10T21:00:00.000Z', }, ], - 'Team: Open Source': [ + 'Product Area: Other': [ { url: 'https://test.com/issues/2', number: 2, title: 'Open Source Issue', - teamLabel: 'Team: Open Source', + productAreaLabel: 'Product Area: Other', triageBy: '2022-12-12T20:00:00.000Z', createdAt: '2022-12-10T21:00:00.000Z', }, @@ -599,13 +599,13 @@ describe('Triage Notification Tests', function () { const now = moment('2022-12-12T17:00:00.000Z'); const postMessageSpy = jest.spyOn(bolt.client.chat, 'postMessage'); await Promise.all( - constructSlackMessage(notificationChannels, teamToIssuesMap, now) + constructSlackMessage(notificationChannels, productAreaToIssuesMap, now) ); expect(postMessageSpy).toHaveBeenCalledTimes(2); await Promise.all( constructSlackMessage( notificationChannels, - teamToIssuesMap, + productAreaToIssuesMap, now.add(1, 'hours') ) ); @@ -613,26 +613,26 @@ describe('Triage Notification Tests', function () { }); it('should return nothing in triage queue if issues were created less than 4 hours ago', async function () { const notificationChannels = { - channel1: ['Team: Test'], - channel2: ['Team: Test', 'Team: Open Source'], + channel1: ['Product Area: Test'], + channel2: ['Product Area: Test', 'Product Area: Other'], }; - const teamToIssuesMap = { - 'Team: Test': [ + const productAreaToIssuesMap = { + 'Product Area: Test': [ { url: 'https://test.com/issues/1', number: 1, title: 'Test Issue', - teamLabel: 'Team: Test', + productAreaLabel: 'Product Area: Test', triageBy: '2022-12-13T21:00:00.000Z', createdAt: '2022-12-12T21:00:00.000Z', }, ], - 'Team: Open Source': [ + 'Product Area: Other': [ { url: 'https://test.com/issues/2', number: 2, title: 'Open Source Issue', - teamLabel: 'Team: Open Source', + productAreaLabel: 'Product Area: Other', triageBy: '2022-12-13T20:00:00.000Z', createdAt: '2022-12-12T21:00:00.000Z', }, @@ -641,32 +641,32 @@ describe('Triage Notification Tests', function () { const now = moment('2022-12-12T16:58:00.000Z'); const postMessageSpy = jest.spyOn(bolt.client.chat, 'postMessage'); await Promise.all( - constructSlackMessage(notificationChannels, teamToIssuesMap, now) + constructSlackMessage(notificationChannels, productAreaToIssuesMap, now) ); expect(postMessageSpy).toHaveBeenCalledTimes(0); }); it('should return all issues in triage queue if SLA is more than 4 hours away', async function () { const notificationChannels = { - channel1: ['Team: Test'], - channel2: ['Team: Test', 'Team: Open Source'], + channel1: ['Product Area: Test'], + channel2: ['Product Area: Test', 'Product Area: Other'], }; - const teamToIssuesMap = { - 'Team: Test': [ + const productAreaToIssuesMap = { + 'Product Area: Test': [ { url: 'https://test.com/issues/1', number: 1, title: 'Test Issue', - teamLabel: 'Team: Test', + productAreaLabel: 'Product Area: Test', triageBy: '2022-12-13T21:00:00.000Z', createdAt: '2022-12-10T21:00:00.000Z', }, ], - 'Team: Open Source': [ + 'Product Area: Other': [ { url: 'https://test.com/issues/2', number: 2, title: 'Open Source Issue', - teamLabel: 'Team: Open Source', + productAreaLabel: 'Product Area: Other', triageBy: '2022-12-13T20:00:00.000Z', createdAt: '2022-12-10T21:00:00.000Z', }, @@ -675,7 +675,7 @@ describe('Triage Notification Tests', function () { const now = moment('2022-12-12T16:58:00.000Z'); const postMessageSpy = jest.spyOn(bolt.client.chat, 'postMessage'); await Promise.all( - constructSlackMessage(notificationChannels, teamToIssuesMap, now) + constructSlackMessage(notificationChannels, productAreaToIssuesMap, now) ); expect(postMessageSpy).toHaveBeenCalledTimes(2); expect(postMessageSpy).toHaveBeenCalledWith({ @@ -766,26 +766,26 @@ describe('Triage Notification Tests', function () { }); it('should not notify if issues are only in triage queue and channel has been notified less than 4 hours ago', async function () { const notificationChannels = { - channel1: ['Team: Test'], - channel2: ['Team: Test', 'Team: Open Source'], + channel1: ['Product Area: Test'], + channel2: ['Product Area: Test', 'Product Area: Other'], }; - const teamToIssuesMap = { - 'Team: Test': [ + const productAreaToIssuesMap = { + 'Product Area: Test': [ { url: 'https://test.com/issues/1', number: 1, title: 'Test Issue', - teamLabel: 'Team: Test', + productAreaLabel: 'Product Area: Test', triageBy: '2022-12-13T21:00:00.000Z', createdAt: '2022-12-10T21:00:00.000Z', }, ], - 'Team: Open Source': [ + 'Product Area: Other': [ { url: 'https://test.com/issues/2', number: 2, title: 'Open Source Issue', - teamLabel: 'Team: Open Source', + productAreaLabel: 'Product Area: Other', triageBy: '2022-12-13T20:00:00.000Z', createdAt: '2022-12-10T21:00:00.000Z', }, @@ -794,13 +794,13 @@ describe('Triage Notification Tests', function () { const now = moment('2022-12-12T16:58:00.000Z'); const postMessageSpy = jest.spyOn(bolt.client.chat, 'postMessage'); await Promise.all( - constructSlackMessage(notificationChannels, teamToIssuesMap, now) + constructSlackMessage(notificationChannels, productAreaToIssuesMap, now) ); expect(postMessageSpy).toHaveBeenCalledTimes(2); await Promise.all( constructSlackMessage( notificationChannels, - teamToIssuesMap, + productAreaToIssuesMap, now.add(2, 'hours') ) ); @@ -808,26 +808,26 @@ describe('Triage Notification Tests', function () { }); it('should notify if issues are only in triage queue and channel has been notified 4 hours ago', async function () { const notificationChannels = { - channel1: ['Team: Test'], - channel2: ['Team: Test', 'Team: Open Source'], + channel1: ['Product Area: Test'], + channel2: ['Product Area: Test', 'Product Area: Other'], }; - const teamToIssuesMap = { - 'Team: Test': [ + const productAreaToIssuesMap = { + 'Product Area: Test': [ { url: 'https://test.com/issues/1', number: 1, title: 'Test Issue', - teamLabel: 'Team: Test', + productAreaLabel: 'Product Area: Test', triageBy: '2022-12-13T21:00:00.000Z', createdAt: '2022-12-10T21:00:00.000Z', }, ], - 'Team: Open Source': [ + 'Product Area: Other': [ { url: 'https://test.com/issues/2', number: 2, title: 'Open Source Issue', - teamLabel: 'Team: Open Source', + productAreaLabel: 'Product Area: Other', triageBy: '2022-12-13T20:00:00.000Z', createdAt: '2022-12-10T21:00:00.000Z', }, @@ -836,13 +836,13 @@ describe('Triage Notification Tests', function () { const now = moment('2022-12-12T16:58:00.000Z'); const postMessageSpy = jest.spyOn(bolt.client.chat, 'postMessage'); await Promise.all( - constructSlackMessage(notificationChannels, teamToIssuesMap, now) + constructSlackMessage(notificationChannels, productAreaToIssuesMap, now) ); expect(postMessageSpy).toHaveBeenCalledTimes(2); await Promise.all( constructSlackMessage( notificationChannels, - teamToIssuesMap, + productAreaToIssuesMap, now.add(4, 'hours') ) ); @@ -850,16 +850,16 @@ describe('Triage Notification Tests', function () { }); it('should return issues appropriately in different blocks', async function () { const notificationChannels = { - channel1: ['Team: Test'], - channel2: ['Team: Test', 'Team: Open Source'], + channel1: ['Product Area: Test'], + channel2: ['Product Area: Test', 'Product Area: Other'], }; - const teamToIssuesMap = { - 'Team: Test': [ + const productAreaToIssuesMap = { + 'Product Area: Test': [ { url: 'https://test.com/issues/1', number: 1, title: 'Test Issue', - teamLabel: 'Team: Test', + productAreaLabel: 'Product Area: Test', triageBy: '2022-12-13T21:00:00.000Z', createdAt: '2022-12-10T21:00:00.000Z', }, @@ -867,17 +867,17 @@ describe('Triage Notification Tests', function () { url: 'https://test.com/issues/3', number: 3, title: 'Test Issue 2', - teamLabel: 'Team: Test', + productAreaLabel: 'Product Area: Test', triageBy: '2022-12-12T19:00:00.000Z', createdAt: '2022-12-10T21:00:00.000Z', }, ], - 'Team: Open Source': [ + 'Product Area: Other': [ { url: 'https://test.com/issues/2', number: 2, title: 'Open Source Issue', - teamLabel: 'Team: Open Source', + productAreaLabel: 'Product Area: Other', triageBy: '2022-12-12T16:00:00.000Z', createdAt: '2022-12-10T21:00:00.000Z', }, @@ -886,7 +886,7 @@ describe('Triage Notification Tests', function () { const now = moment('2022-12-12T16:58:00.000Z'); const postMessageSpy = jest.spyOn(bolt.client.chat, 'postMessage'); await Promise.all( - constructSlackMessage(notificationChannels, teamToIssuesMap, now) + constructSlackMessage(notificationChannels, productAreaToIssuesMap, now) ); expect(postMessageSpy).toHaveBeenCalledTimes(2); expect(postMessageSpy).toHaveBeenCalledWith({ diff --git a/src/webhooks/pubsub/index.ts b/src/webhooks/pubsub/index.ts index b56f70ea..b90797fb 100644 --- a/src/webhooks/pubsub/index.ts +++ b/src/webhooks/pubsub/index.ts @@ -7,8 +7,8 @@ import { ClientType } from '@/api/github/clientType'; import { getLabelsTable } from '@/brain/issueNotifier'; import { OWNER, + PRODUCT_AREA_LABEL_PREFIX, SENTRY_REPO, - TEAM_LABEL_PREFIX, UNTRIAGED_LABEL, } from '@/config'; import { Issue } from '@/types'; @@ -19,7 +19,7 @@ import { db } from '@utils/db'; const DEFAULT_REPOS = [SENTRY_REPO]; const GH_API_PER_PAGE = 100; -const DEFAULT_TEAM_LABEL = 'Team: Open Source'; +const DEFAULT_PRODUCT_AREA_LABEL = 'Product Area: Other'; const getChannelLastNotifiedTable = () => db('channel_last_notified'); type PubSubPayload = { @@ -48,7 +48,7 @@ type IssueSLOInfo = { url: string; number: number; title: string; - teamLabel: string; + productAreaLabel: string; triageBy: string; createdAt: string; }; @@ -82,11 +82,11 @@ export const opts = { const getLabelName = (label?: Issue['labels'][number]) => typeof label === 'string' ? label : label?.name || ''; -const getIssueTeamLabel = (issue: Issue) => { +const getIssueProductAreaLabel = (issue: Issue) => { const label = issue.labels.find((label) => - getLabelName(label).startsWith(TEAM_LABEL_PREFIX) + getLabelName(label).startsWith(PRODUCT_AREA_LABEL_PREFIX) ); - return getLabelName(label) || DEFAULT_TEAM_LABEL; + return getLabelName(label) || DEFAULT_PRODUCT_AREA_LABEL; }; export const getTriageSLOTimestamp = async ( @@ -125,7 +125,7 @@ export const getTriageSLOTimestamp = async ( export const constructSlackMessage = ( notificationChannels: Record, - teamToIssuesMap: Record, + productAreaToIssuesMap: Record, now: moment.Moment ) => { return Object.keys(notificationChannels).flatMap(async (channelId) => { @@ -135,8 +135,8 @@ export const constructSlackMessage = ( const actFastIssues: SlackMessageIssueItem[] = []; const triageQueueIssues: SlackMessageIssueItem[] = []; if (await isChannelInBusinessHours(channelId, now)) { - notificationChannels[channelId].map((team) => { - teamToIssuesMap[team].forEach( + notificationChannels[channelId].map((productArea) => { + productAreaToIssuesMap[productArea].forEach( ({ url, number, title, triageBy, createdAt }) => { // Escape issue title for < and > characters const escapedIssueTitle = title @@ -411,7 +411,7 @@ export const constructSlackMessage = ( }); }; -export const notifyTeamsForUntriagedIssues = async ( +export const notifyProductOwnersForUntriagedIssues = async ( request: FastifyRequest<{ Body: { message: { data: string } } }>, reply: FastifyReply ) => { @@ -460,7 +460,7 @@ export const notifyTeamsForUntriagedIssues = async ( url: issue.html_url, number: issue.number, title: issue.title, - teamLabel: getIssueTeamLabel(issue), + productAreaLabel: getIssueProductAreaLabel(issue), triageBy: await getTriageSLOTimestamp(octokit, repo, issue.number), createdAt: issue.created_at, })); @@ -472,33 +472,33 @@ export const notifyTeamsForUntriagedIssues = async ( await Promise.all(repos.map(getIssueSLOInfoForRepo)) ).flat(); - // Get an N-to-N mapping of "Team: *" labels to issues - const teamToIssuesMap: Record = {}; - const teamsToNotify = new Set() as Set; + // Get an N-to-N mapping of "Product Area: *" labels to issues + const productAreaToIssuesMap: Record = {}; + const productAreasToNotify = new Set() as Set; issuesToNotifyAbout.forEach((data) => { - if (data.teamLabel in teamToIssuesMap) { - teamToIssuesMap[data.teamLabel].push(data); + if (data.productAreaLabel in productAreaToIssuesMap) { + productAreaToIssuesMap[data.productAreaLabel].push(data); } else { - teamToIssuesMap[data.teamLabel] = [data]; - teamsToNotify.add(data.teamLabel); + productAreaToIssuesMap[data.productAreaLabel] = [data]; + productAreasToNotify.add(data.productAreaLabel); } }); - // Get a mapping from Channels to subscribed teams + // Get a mapping from Channels to subscribed product areas const notificationChannels: Record = ( await getLabelsTable() .select('label_name', 'channel_id') - .whereIn('label_name', Array.from(teamsToNotify)) + .whereIn('label_name', Array.from(productAreasToNotify)) ).reduce((res, { label_name, channel_id }) => { - const teams = res[channel_id] || []; - teams.push(label_name); - res[channel_id] = teams; + const productAreas = res[channel_id] || []; + productAreas.push(label_name); + res[channel_id] = productAreas; return res; }, {}); - // Notify all channels associated with the relevant `Team: *` label per issue + // Notify all channels associated with the relevant `Product Area: *` label per issue const notifications = constructSlackMessage( notificationChannels, - teamToIssuesMap, + productAreaToIssuesMap, now ); // Do all this in parallel and wait till all finish