Skip to content

Commit

Permalink
chore: increase look back period for customer issues needing triage (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
RogerHYang authored Nov 8, 2024
1 parent d2ff57a commit f2bd3a7
Show file tree
Hide file tree
Showing 3 changed files with 205 additions and 130 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.github/
194 changes: 194 additions & 0 deletions .github/.scripts/collect-customer-issues.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
// Utility function to create a hyperlink to a GitHub user
function createGithubLink(username) {
return `<https://github.com/${username}|${username}>`;
}

// Helper function to check if an issue has any of the required labels
function hasLabel(issueLabels, requiredLabels) {
return issueLabels.some((label) => requiredLabels.includes(label.name));
}

// Helper function to calculate the difference in days and format as "Today," "Yesterday," or "X days ago"
function formatDateDifference(date) {
const now = new Date();
const millisecondsInADay = 1000 * 60 * 60 * 24;
const daysDifference = Math.floor(
(now - new Date(date)) / millisecondsInADay,
);

if (daysDifference === 0) return "Today";
if (daysDifference === 1) return "Yesterday";
return `${daysDifference} days ago`;
}

// Helper function to get a sorted list of unique participants from comments on an issue, excluding the author
async function getParticipants(github, context, issueNumber, author) {
try {
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
per_page: 100,
});

const participants = new Set(
comments
.map((comment) => comment.user.login)
.filter((username) => username !== author), // Exclude the author
);
return Array.from(participants).sort();
} catch (error) {
console.error(`Error fetching comments for issue #${issueNumber}:`, error);
return [];
}
}

// Helper function to filter issues based on required labels and a cutoff date
function filterIssues(issues, requiredLabels, cutoffDate) {
return issues
.filter((issue) => {
const hasLabels = hasLabel(issue.labels, requiredLabels);
const isRecent = new Date(issue.created_at) > cutoffDate;
const isNotPR = !issue.pull_request;
return hasLabels && isRecent && isNotPR;
})
.sort((a, b) => b.number - a.number); // Sort by issue number, newest first
}

// Helper function to separate issues into "stale" and "fresh" based on staleness threshold
function splitIssuesByStaleness(issues, stalenessThreshold) {
const staleIssues = [];
const freshIssues = [];
const now = new Date();

issues.forEach((issue) => {
const daysOld = Math.floor(
(now - new Date(issue.created_at)) / (1000 * 60 * 60 * 24),
);
if (daysOld > stalenessThreshold) {
staleIssues.push(issue);
} else {
freshIssues.push(issue);
}
});

return { staleIssues, freshIssues };
}

// Helper function to categorize fresh issues as either "bug" or "enhancement/inquiry"
function categorizeIssues(issues) {
const bugIssues = [];
const enhancementIssues = [];

issues.forEach((issue) => {
if (issue.labels.some((label) => label.name === "bug")) {
bugIssues.push(issue);
} else {
enhancementIssues.push(issue);
}
});

return { bugIssues, enhancementIssues };
}

// Helper function to build a detailed description for each issue
async function formatIssueLine(github, context, issue, index) {
let line = `${index + 1}. *<${issue.html_url}|#${issue.number}>:* ${issue.title}`;
line += ` (by ${createGithubLink(issue.user.login)}; ${formatDateDifference(issue.created_at)})`;

if (issue.comments > 0) {
line += `; ${issue.comments} comment${issue.comments > 1 ? "s" : ""}`;
const participants = await getParticipants(
github,
context,
issue.number,
issue.user.login,
);
if (participants.length > 0) {
const participantLinks = participants.map(createGithubLink).join(", ");
line += `; participants: ${participantLinks}`;
}
}

if (issue.assignees.length > 0) {
const assigneeLinks = issue.assignees
.map((assignee) => createGithubLink(assignee.login))
.join(", ");
line += `; assigned to: ${assigneeLinks}`;
}

return line;
}

// Helper function to build a message for Slack with grouped and formatted issues
async function buildSlackMessage(github, context, issueGroups, lookbackDays) {
const messageLines = [
`*🛠️ Phoenix Customer Issues Opened in the Last ${lookbackDays} Day(s) Pending <https://github.com/Arize-ai/phoenix/issues?q=is%3Aissue+is%3Aopen+label%3Atriage|Triage>*\n`,
];

for (const [issuesArray, header] of issueGroups) {
if (issuesArray.length > 0) {
messageLines.push(header); // Add the group header (e.g., "🐛 Bugs")
const issueDescriptions = await Promise.all(
issuesArray.map((issue, index) =>
formatIssueLine(github, context, issue, index),
),
);
messageLines.push(...issueDescriptions);
}
}

return messageLines.join("\n");
}

// Main function to fetch and format issues, then send the Slack message
module.exports = async ({ github, context, core }) => {
const requiredLabels = ["triage"];
const lookbackDays = parseInt(process.env.LOOKBACK_DAYS || "120", 10);
const stalenessThreshold = parseInt(
process.env.STALENESS_THRESHOLD_IN_DAYS || "14",
10,
);

const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - lookbackDays);

// Retrieve issues created within the specified lookback period
const issues = await github.paginate(github.rest.issues.listForRepo, {
owner: context.repo.owner,
repo: context.repo.repo,
state: "open",
since: cutoffDate.toISOString(),
per_page: 100,
});

// Filter issues by label and date, then categorize by staleness and type
const filteredIssues = filterIssues(issues, requiredLabels, cutoffDate);
if (filteredIssues.length === 0) {
core.setOutput("has_issues", "false");
return;
}

core.setOutput("has_issues", "true");

const { staleIssues, freshIssues } = splitIssuesByStaleness(
filteredIssues,
stalenessThreshold,
);
const { bugIssues, enhancementIssues } = categorizeIssues(freshIssues);

const issueGroups = [
[bugIssues, "*🐛 Bugs*"],
[enhancementIssues, "*💡 Enhancements or Inquiries*"],
[staleIssues, `*🥀 Stale Issues (>${stalenessThreshold} days)*`],
];

// Build the Slack message and set as output
const message = await buildSlackMessage(
github,
context,
issueGroups,
lookbackDays,
);
core.setOutput("slack_message", message);
};
140 changes: 10 additions & 130 deletions .github/workflows/collect-customer-issues.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,149 +2,29 @@ name: Collect customer issues

on:
schedule:
- cron: "0 15 * * *"
- cron: "0 15 * * 1-5"
workflow_dispatch:

jobs:
collect-issues-pending-triage:
name: Pending Triage
runs-on: ubuntu-latest
env:
DAYS: 30
LOOKBACK_DAYS: 120
STALENESS_THRESHOLD_IN_DAYS: 14
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
sparse-checkout: |
.github/
- name: Retrieve and format issues
id: retrieve-issues
uses: actions/github-script@v7
with:
script: |
// List of labels to filter by
const requiredLabels = [
"triage",
];
// List of core developers to exclude
const coreDevelopers = [
"mikeldking",
"axiomofjoy",
"anticorrelator",
"cephalization",
"Parker-Stafford",
"Jgilhuly",
"RogerHYang",
];
// Access the DAYS environment variable
const days = parseInt(process.env.DAYS || '7', 10);
// Calculate the cutoff date
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
// Fetch issues created since DAYS ago
const issues = await github.paginate(
github.rest.issues.listForRepo,
{
owner: context.repo.owner,
repo: context.repo.repo,
state: "open",
since: cutoffDate.toISOString(),
per_page: 100,
}
);
// Check if issue has any of the required labels
function hasRequiredLabel(issueLabels, requiredLabels) {
return issueLabels.some(label => requiredLabels.includes(label.name));
}
// Filter issues
const filteredIssues = issues.filter(issue =>
!coreDevelopers.includes(issue.user.login) &&
hasRequiredLabel(issue.labels, requiredLabels) &&
new Date(issue.created_at) > cutoffDate &&
!issue.pull_request
).sort((a, b) => b.number - a.number);
// Function to calculate "X days ago" from created_at date
function timeAgo(createdAt) {
const createdDate = new Date(createdAt);
const now = new Date();
const diffInMs = now - createdDate;
const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24));
if (diffInDays === 0) {
return "Today";
} else if (diffInDays === 1) {
return "Yesterday";
} else {
return `${diffInDays} days ago`;
}
}
// Function to get unique participants from comments on an issue, excluding the author
async function getParticipants(issueNumber, author) {
try {
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
per_page: 100,
});
// Extract unique usernames of commenters, excluding the author
const uniqueParticipants = [
...new Set(comments.map(comment => comment.user.login).filter(username => username !== author))
].sort();
return uniqueParticipants;
} catch (error) {
console.error(`Error fetching comments for issue #${issueNumber}: ${error}`);
return [];
}
}
// Format the issues as a Markdown message for Slack
if (filteredIssues.length === 0) {
core.setOutput("has_issues", "false");
} else {
core.setOutput("has_issues", "true");
let message = `*🛠️ Phoenix Customer Issues Opened in the Last ${days} Day(s)`;
message += ` Pending <https://github.com/Arize-ai/phoenix/issues?q=is%3Aissue+is%3Aopen+label%3Atriage|Triage>`;
message += `*\n\n`;
// Separate issues into two lists: those with the "bug" label and those without
const bugIssues = filteredIssues.filter(issue =>
issue.labels.some(label => label.name === 'bug')
);
const enhancementIssues = filteredIssues.filter(issue =>
!issue.labels.some(label => label.name === 'bug')
);
const issueGroups = [
[bugIssues, "*🐛 Bugs*"],
[enhancementIssues, "*💡 Enhancements or Inquiries*"]
];
// Use `for...of` loop to allow async/await inside the loop
for (const [issues, header] of issueGroups) {
if (issues.length > 0) {
message += `${header}\n`;
for (const [i, issue] of issues.entries()) {
message += `${i + 1}. *<${issue.html_url}|#${issue.number}>:* ${issue.title}`;
message += ` (by <https://github.com/${issue.user.login}|${issue.user.login}>`;
message += `; ${timeAgo(issue.created_at)}`;
if (issue.comments > 0) {
message += `; ${issue.comments} comments`;
const participants = await getParticipants(issue.number, issue.user.login);
if (participants.length > 0) {
message += `; participants: `;
message += participants.map(participant => `<https://github.com/${participant}|${participant}>`).join(", ");
}
}
if (issue.assignees.length > 0) {
message += `; assigned to: `
message += issue.assignees.map(assignee => `<https://github.com/${assignee.login}|${assignee.login}>`).join(", ");
}
message += `)\n`;
}
}
}
core.setOutput("slack_message", message);
}
const script = require(".github/.scripts/collect-customer-issues.js");
await script({github, context, core});
- name: Send message to Slack
uses: slackapi/slack-github-action@v1
if: steps.retrieve-issues.outputs.has_issues == 'true'
Expand Down

0 comments on commit f2bd3a7

Please sign in to comment.