Skip to content

Commit

Permalink
move to using escalation policies instead of schedules
Browse files Browse the repository at this point in the history
  • Loading branch information
after-ephemera committed Nov 25, 2024
1 parent 9e99d96 commit 3c71373
Show file tree
Hide file tree
Showing 9 changed files with 3,480 additions and 80 deletions.
3,362 changes: 3,362 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

18 changes: 11 additions & 7 deletions src/api/oncall/index.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
import SlackApi, { type Member, OncallSlackUser } from "@api/slack";
import { type PdOncallResult } from "@types";
import pagerDuty from "@api/pd";
import SlackApi, { type Member, OncallSlackUser } from '@api/slack';
import { type PdOncallResult } from '@types';
import pagerDuty from '@api/pd';

export const getOncallSlackMembers = async (): Promise<OncallSlackUser[]> => {
const oncallSlackMembers: OncallSlackUser[] = [];
const oncallSlackerNames: string[] = [];
const pdUsers: PdOncallResult[] = await pagerDuty.getOncalls(null);
console.debug(`pd users: ${JSON.stringify(pdUsers, null, 2)}`);
const slack = new SlackApi();
for (const pdUser of pdUsers) {
try {
console.debug(
`getting slack user for ${pdUser.user.email}: ${pdUser.escalation_policy.id}`
);
const slackUser: Member = await slack.getUser(pdUser.user.email);
oncallSlackMembers.push(
new OncallSlackUser(
pdUser.user.name,
pdUser.user.email,
pdUser.user.id,
pdUser.schedule.id,
slackUser.id ?? ""
slackUser.id ?? '',
pdUser.escalation_policy.id
)
);
if (slackUser && slackUser.name !== undefined) {
if (slackUser.name !== undefined) {
oncallSlackerNames.push(slackUser.name);
}
} catch (e) {
console.error(`Error getting slack user for ${pdUser.user.email}`);
console.error(`Error getting slack user for ${pdUser.user.email}: ${e}`);
}
}
return oncallSlackMembers;
Expand Down
23 changes: 15 additions & 8 deletions src/api/pd/ls.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type BotConfig } from "@types";
import { type OncallSlackUser } from "@api/slack";
import jsonConfig from "config";
import { type BotConfig } from '@types';
import { type OncallSlackUser } from '@api/slack';
import jsonConfig from 'config';

const config: BotConfig = jsonConfig as BotConfig;
type OncallMap = Record<string, string>;
Expand Down Expand Up @@ -29,18 +29,25 @@ export const makeOncallMappingMessage = (
const shortnamesMap = transformMapping(oncallMap);
return (
Object.entries(shortnamesMap)
.map(([pdScheduleId, shortnames]) => [
.map(([pdEscalationPolicyId, shortnames]) => [
shortnames,
oncallSlackMembers.find((s) => s.pdScheduleId === pdScheduleId),
oncallSlackMembers.find(
(s) => s.pdEscalationPolicyId === pdEscalationPolicyId
),
])
// remove null and undefined
.filter(([_, id]: Array<string[] | OncallSlackUser | undefined>) => id !== null || id !== undefined)
.filter(([_, id]: Array<string[] | OncallSlackUser | undefined>) => {
if (id === undefined || id === null) {
console.debug(`filtering id: ${id}`);
}
return id !== null && id !== undefined;
})
.map(
([shortnames, s]: Array<string[] | OncallSlackUser | undefined>) =>
`(${(shortnames as string[]).join(" | ")}): @${
`(${(shortnames as string[]).join(' | ')}): @${
(s as OncallSlackUser)?.name
}`
)
.join("\n")
.join('\n')
);
};
51 changes: 31 additions & 20 deletions src/api/pd/pagerduty.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { type BotConfig } from "../../types";
import NodeCache from "node-cache";
import jsonConfig from "config";
import { type BotConfig } from '../../types';
import NodeCache from 'node-cache';
import jsonConfig from 'config';

const config: BotConfig = jsonConfig as BotConfig;

const oncallsParams: Record<string, string | number> = {
time_zone: "UTC",
"include[]": "users",
time_zone: 'UTC',
'include[]': 'users',
offset: 0,
limit: 100000,
total: 'true',
};

export interface PdUser {
Expand All @@ -21,10 +22,15 @@ interface PdSchedule {
id: string;
}

interface EscalationPolicy {
id?: string;
}

export interface PdOncallResult {
id?: string;
user: PdUser;
schedule: PdSchedule;
schedule?: PdSchedule;
escalation_policy: EscalationPolicy;
}

interface OncallOptions {
Expand All @@ -49,18 +55,18 @@ class PagerDuty {

constructor(options: any) {
this.headers = new Headers({
Accept: "application/vnd.pagerduty+json;version=2",
"Content-Type": "application/json",
Authorization: "Token token=" + options.pagerduty_token,
Accept: 'application/vnd.pagerduty+json;version=2',
'Content-Type': 'application/json',
Authorization: 'Token token=' + options.pagerduty_token,
});
this.endpoint = "https://api.pagerduty.com";
this.endpoint = 'https://api.pagerduty.com';
this.cache = new NodeCache();
this.token = options.pagerduty_token;
this.cacheInterval = options.cache_interval_seconds;
}

async getAllPaginatedData(options: OncallOptions): Promise<PdOncallResult[]> {
console.debug("getAllPaginatedData");
console.debug('getAllPaginatedData');
options.params.limit = 100; // 100 is the max limit allowed by pagerduty
options.params.offset = 0;

Expand All @@ -75,12 +81,12 @@ class PagerDuty {
content?: any
): Promise<PdOncallResult[]> => {
if (error !== null) {
console.debug("Issues with pagedCallback: " + error);
console.debug('Issues with pagedCallback: ' + error);
return error;
}

if (content?.[options.contentIndex] === undefined) {
error = "Page does not have valid data: " + JSON.stringify(content);
error = 'Page does not have valid data: ' + JSON.stringify(content);
console.debug(error);
return error;
}
Expand All @@ -98,25 +104,30 @@ class PagerDuty {
});

if (options.params.offset >= total) {
console.debug(
`returning items because offset: ${
options.params.offset
} >= total: ${JSON.stringify(content, null, 2)}`
);
return items;
} else {
return await requestAnotherPage();
}
};

const requestAnotherPage = async (): Promise<PdOncallResult[]> => {
console.debug("requesting another page");
// must use node's built in querystring since qs doesn't build arrays like PagerDuty expects.
const url =
this.endpoint +
options.uri +
"?" +
'?' +
new URLSearchParams({
...options.params,
offset: options.params.offset.toString(),
limit: options.params.limit.toString(),
}).toString();
requestOptions.url = url;
console.debug(`requesting another page: ${url}`);

const response = await fetch(url, requestOptions);
if (!response.ok) {
Expand All @@ -130,11 +141,11 @@ class PagerDuty {
}

async getOncalls(params?: any): Promise<PdOncallResult[]> {
console.debug("pagerduty.getOnCalls");
console.debug('pagerduty.getOnCalls');
const options = {
contentIndex: "oncalls",
secondaryIndex: "user",
uri: "/oncalls",
contentIndex: 'oncalls',
secondaryIndex: 'user',
uri: '/oncalls',
params: params ?? oncallsParams,
};
let oncalls = this.cache.get(options.contentIndex);
Expand All @@ -146,5 +157,5 @@ class PagerDuty {
}
}

const pagerDuty: PagerDuty = new PagerDuty(config.get("pagerduty"));
const pagerDuty: PagerDuty = new PagerDuty(config.get('pagerduty'));
export default pagerDuty;
29 changes: 15 additions & 14 deletions src/api/slack/index.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
import app from "@src/app";
import jsonConfig from "config";
import { type BotConfig, type Email } from "@types";
import { type UsersListResponse } from "@slack/web-api";
import { type Member } from "@slack/web-api/dist/response/UsersListResponse";
import NodeCache from "node-cache";
import app from '@src/app';
import jsonConfig from 'config';
import { type BotConfig, type Email } from '@types';
import { type UsersListResponse } from '@slack/web-api';
import { type Member } from '@slack/web-api/dist/response/UsersListResponse';
import NodeCache from 'node-cache';
const config: BotConfig = jsonConfig as BotConfig;

export type { Member } from "@slack/web-api/dist/response/UsersListResponse";
export type { Member } from '@slack/web-api/dist/response/UsersListResponse';

export class OncallSlackUser {
name: string;
email: string;
pdId: string;
pdScheduleId: string;
slackId: string;
pdEscalationPolicyId?: string;

constructor(
name: string,
email: string,
pdId: string,
pdScheduleId: string,
slackId: string
slackId: string,
pdEscalationPolicyId: string | undefined
) {
this.name = name;
this.email = email;
this.pdId = pdId;
this.pdScheduleId = pdScheduleId;
this.pdEscalationPolicyId = pdEscalationPolicyId;
this.slackId = slackId;
}
}
Expand All @@ -42,7 +42,7 @@ export default class SlackApi {

getUsers = async (): Promise<UsersListResponse> => {
const cachedUsers: UsersListResponse | undefined =
this.cache.get("allUsers");
this.cache.get('allUsers');
if (cachedUsers !== undefined) {
return cachedUsers;
}
Expand All @@ -53,18 +53,19 @@ export default class SlackApi {
.then((result) => {
return result;
});
this.cache.set("allUsers", usersResult, this.cacheInterval);
this.cache.set('allUsers', usersResult, this.cacheInterval);
return await usersResult;
};

getUser = async (email: Email): Promise<Member> => {
console.debug(`getUser: ${email}`);
const cachedUser = this.cache.get(email);
if (cachedUser !== undefined) {
return cachedUser as Member;
} else {
const allUsers = await this.getUsers();
if (allUsers.members === undefined) {
throw new Error("No users found");
throw new Error('No users found');
}
const user = allUsers.members.find(
(u: Member) => u.profile?.email === email
Expand Down
35 changes: 22 additions & 13 deletions src/listeners/common/ls.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import { getOncallSlackMembers } from "@api/oncall";
import { makeOncallMappingMessage } from "@api/pd";
import { getOncallSlackMembers } from '@api/oncall';
import { makeOncallMappingMessage } from '@api/pd';


const handleLsCommand = async (
threadTs: string,
say:Function,
) => {
const slackMembers = await getOncallSlackMembers();
const usersMessage = makeOncallMappingMessage(slackMembers);
await say({
text: `Current oncall listing:\n ${usersMessage}`,
thread_ts: threadTs,
});
const handleLsCommand = async (threadTs: string, say: Function) => {
const slackMembers = await getOncallSlackMembers();
const usersMessage = makeOncallMappingMessage(slackMembers);
console.log(`total slack members: ${slackMembers.length}`);
// console.log('slack members:', JSON.stringify(slackMembers, null, 2));
console.log(
`tom user: ${JSON.stringify(
slackMembers.find(
(s) =>
s.name.toLowerCase().includes('thom') ||
s.name.toLowerCase().includes('tom')
),
null,
2
)}`
);
await say({
text: `Current oncall listing:\n ${usersMessage}`,
thread_ts: threadTs,
});
};

export default handleLsCommand;
14 changes: 7 additions & 7 deletions src/listeners/events/app-mentioned.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import {
type AllMiddlewareArgs,
type SlackEventMiddlewareArgs,
} from "@slack/bolt";
import handleVersionCommand from "@srclisteners/common/version";
import handleLsCommand from "@srclisteners/common/ls";
import handleHelpCommand from "@srclisteners/common/help";
} from '@slack/bolt';
import handleVersionCommand from '@srclisteners/common/version';
import handleLsCommand from '@srclisteners/common/ls';
import handleHelpCommand from '@srclisteners/common/help';

const USER_MENTION_REGEX = "^<@U[A-Z0-9]{8,10}>";
const USER_MENTION_REGEX = '^<@U[A-Z0-9]{8,10}>';
const VERSION_REGEX = new RegExp(`${USER_MENTION_REGEX} version`);
const LS_REGEX = new RegExp(`${USER_MENTION_REGEX} ls`);

const appMentionedCallback = async ({
event,
say,
}: AllMiddlewareArgs &
SlackEventMiddlewareArgs<"app_mention">): Promise<void> => {
SlackEventMiddlewareArgs<'app_mention'>): Promise<void> => {
console.log(`**** bot mentioned ${event.text}`);
const threadTs = event.ts;
if (event.text.match(VERSION_REGEX) !== null) {
Expand All @@ -23,7 +23,7 @@ const appMentionedCallback = async ({
await handleLsCommand(threadTs, say);
} else {
// list available commands
handleHelpCommand(threadTs, say);
await handleHelpCommand(threadTs, say);
}
};

Expand Down
Loading

0 comments on commit 3c71373

Please sign in to comment.