Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Generic stats mode (analyzes any reactions) #4

Open
wants to merge 20 commits into
base: TO-BE-DEPRECATED_master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": [
"standard"
],
"rules": {
"no-multi-spaces": [
"error",
{
"ignoreEOLComments": true
}
]
}
}
13 changes: 13 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Node: Nodemon",
"processId": "${command:PickProcess}",
"restart": true,
"protocol": "inspector",
},
]
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This application has a few entry points and features to help you keep track of r

| Name and Description | Visual |
|------------------------ |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **1. Scheduled Reminders** <br> You can invite the bot to public channels and it will monitor for and remind the channel about messages that fit specific criteria on a scheduled basis.<br><br>All messages, except those posted by the app are counted, so this works great with [Slack's Workflow Builder](https://slack.com/slack-tips/quickly-field-requests-for-your-team) and any other monitoring integration that you use that uses the emojis you configure. | [![](docs/assets/func_1_scheduled_reminders.png)](docs/assets/func_1_scheduled_reminders.png) |
| **1. Scheduled Jobs (aka Reminders)** <br> You can invite the bot to public channels and it will monitor for and remind the channel about messages that fit specific criteria on a scheduled basis.<br><br>All messages, except those posted by the app are counted, so this works great with [Slack's Workflow Builder](https://slack.com/slack-tips/quickly-field-requests-for-your-team) and any other monitoring integration that you use that uses the emojis you configure. | [![](docs/assets/func_1_scheduled_jobs.png)](docs/assets/func_1_scheduled_jobs.png) |
| **2. Ad-hoc Reporting** <br> You can use the global shortcut :zap: to create ad-hoc reports on any public channel. It'll give you top-line message counts by urgency and status and provide a CSV for offline analysis too.  <ol type="a"><li>Trigger the modal with a [global shortcut](https://slackhq.com/speed-up-work-with-apps-for-slack) and configure your report in the resulting modal</li><li>Triage stats bot will be added to the specified channel and run its analysis</li><li>Triage stats will be delivered to you in a DM from the bot</li></ol> | [![](docs/assets/func_2_ad_hoc_reports.gif)](docs/assets/func_2_ad_hoc_reports.gif) |
| **3. View Configuration** <br> The app's [Slack App Home](https://api.slack.com/surfaces/tabs) offers users a view into the configuration of the application | [![](docs/assets/func_3_app_home.png)](docs/assets/func_3_app_home.png) |

Expand Down
102 changes: 53 additions & 49 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const triageConfig = require('./config')
const modalViews = require('./views/modals.blockkit')
const appHomeView = require('./views/app_home.blockkit')
const { getAllMessagesForPastHours, filterAndEnrichMessages, messagesToCsv } = require('./helpers/messages')
const { scheduleReminders, manuallyTriggerScheduledJobs } = require('./helpers/scheduled_jobs')
const { scheduleJobs, manuallyTriggerScheduledJobs } = require('./helpers/scheduled_jobs')

// ====================================
// === Initialization/Configuration ===
Expand Down Expand Up @@ -52,7 +52,7 @@ const app = new App({
let teamData = installation.team
teamData = Object.assign(teamData, installation)
delete teamData.team // we already have this information from the assign above
delete teamData.user.token // we dont want a user token, if the scopes are requested
delete teamData.user.token // we don't want a user token, if the scopes are requested

// Do an upsert so that we always have just one document per team ID
await AuthedTeam.findOneAndUpdate({ id: teamData.id }, teamData, { upsert: true })
Expand All @@ -72,19 +72,19 @@ const app = new App({
// =========================================================================

// Handle the shortcut we configured in the Slack App Config
app.shortcut('triage_stats', async ({ ack, context, body }) => {
app.shortcut('channel_stats', async ({ ack, context, body }) => {
// Acknowledge right away
await ack()

// Open a modal
await app.client.views.open({
token: context.botToken,
trigger_id: body.trigger_id,
view: modalViews.select_triage_channel
view: modalViews.select_channel_and_config
})
})

// Handle `view_submision` of modal we opened as a result of the `triage_stats` shortcut
// Handle `view_submision` of modal we opened as a result of the `channel_stats` shortcut
app.view('channel_selected', async ({ body, view, ack, client, logger, context }) => {
// Acknowledge right away
await ack()
Expand All @@ -94,9 +94,11 @@ app.view('channel_selected', async ({ body, view, ack, client, logger, context }
view.state.values.channel.channel.selected_conversation
const nHoursToGoBack =
parseInt(view.state.values.n_hours.n_hours.selected_option.value) || 7
const statsType =
view.state.values.stats_type.stats_type.selected_option.value

try {
// Get converstion info; this will throw an error if the bot does not have access to it
// Get conversation info; this will throw an error if the bot does not have access to it
const conversationInfo = await client.conversations.info({
channel: selectedChannelId,
include_num_members: true
Expand All @@ -110,79 +112,81 @@ app.view('channel_selected', async ({ body, view, ack, client, logger, context }
// Let the user know, in a DM from the bot, that we're working on their request
const msgWorkingOnIt = await client.chat.postMessage({
channel: submittedByUserId,
text: `*You asked for triage stats for <#${selectedChannelId}>*.\n` +
text: `*You asked for _${statsType} stats_ for <#${selectedChannelId}>*.\n` +
`I'll work on the stats for the past ${nHoursToGoBack} hours right away!`
})

// Thread a message while we get to work on the analysis
await client.chat.postMessage({
channel: msgWorkingOnIt.channel,
thread_ts: msgWorkingOnIt.ts,
text: `A number for you while you wait.. the channel has ${conversationInfo.channel.num_members} members (including apps) currently`
text: `A number for you while you wait.. <#${selectedChannelId}> has ${conversationInfo.channel.num_members} members (including apps) currently`
})

// Get all messages from the beginning of time (probably not a good idea)
// Get all messages for the time period specified
const allMessages = await getAllMessagesForPastHours(
selectedChannelId,
nHoursToGoBack,
client
)

// Use a helper method to enrich the messages we have
const allMessagesEnriched = filterAndEnrichMessages(allMessages, selectedChannelId, context.botId)

// For each level, let's do some analysis!
const levelDetailBlocks = []
for (const i in triageConfig._.levels) {
const level = triageConfig._.levels[i]
const allMessagesForLevel = allMessagesEnriched.filter(
m => m[`_level_${level}`] === true
)

// Formulate strings for each status
const countsStrings = triageConfig._.statuses.map(status => {
const messagesForLevelAndStatus = allMessagesForLevel.filter(
m => m[`_status_${status}`] === true
const allMessagesEnriched = filterAndEnrichMessages(allMessages, selectedChannelId, context.botId, statsType)

if (statsType === 'triage') {
// For each level, let's do some analysis!
const levelDetailBlocks = []
for (const i in triageConfig._.levels) {
const level = triageConfig._.levels[i]
const allMessagesForLevel = allMessagesEnriched.filter(
m => m[`_level_${level}`] === true
)
return `\tMessages ${status} ${triageConfig._.statusToEmoji[status]}: ${messagesForLevelAndStatus.length}`
})

// Add level block to array
levelDetailBlocks.push(
{
// Formulate strings for each status
const countsStrings = triageConfig._.statuses.map(status => {
const messagesForLevelAndStatus = allMessagesForLevel.filter(
m => m[`_status_${status}`] === true
)
return `\tMessages ${status} ${triageConfig._.statusToEmoji[status]}: ${messagesForLevelAndStatus.length}`
})

// Add level block to array
levelDetailBlocks.push(
{
type: 'section',
text: {
type: 'mrkdwn',
text: `${triageConfig._.levelToEmoji[level]} *${level}* (${allMessagesForLevel.length} total)\n${countsStrings.join('\n')}`
}
}
)
}

// Send a single message to the thread with all of the stats by level
await client.chat.postMessage({
channel: msgWorkingOnIt.channel,
thread_ts: msgWorkingOnIt.ts,
blocks: [{
type: 'section',
text: {
type: 'mrkdwn',
text: `${triageConfig._.levelToEmoji[level]} *${level}* (${allMessagesForLevel.length} total)\n${countsStrings.join('\n')}`
text: "Here's a summary of the messages needing attention by urgency level and status:"
}
}
)
}].concat(levelDetailBlocks)
})
}

// Send a single message to the thread with all of the stats by level
await client.chat.postMessage({
channel: msgWorkingOnIt.channel,
thread_ts: msgWorkingOnIt.ts,
blocks: [{
type: 'section',
text: {
type: 'mrkdwn',
text: "Here's a summary of the messages needing attention by urgency level and status:"
}
}].concat(levelDetailBlocks)
})

// Try to parse our object to CSV and upload it as an attachment
try {
// Convert object to CSV
const csvString = messagesToCsv(allMessagesEnriched)
const csvString = messagesToCsv(allMessagesEnriched, statsType)

// Upload CSV File
await client.files.upload({
channels: msgWorkingOnIt.channel,
content: csvString,
title: `All messages from the past ${nHoursToGoBack} hours`,
filename: 'allMessages.csv',
filename: `${selectedChannelId}_last${nHoursToGoBack}hours_allMessages_${statsType}.csv`,
filetype: 'csv',
thread_ts: msgWorkingOnIt.ts
})
Expand Down Expand Up @@ -232,7 +236,7 @@ app.event('app_home_opened', async ({ payload, context, logger }) => {
})

// Handle the shortcut for triggering manually scheduled jobs;
// this should only be used for debugging (so we dont have to wait until a triggered job would normally fire)
// this should only be used for debugging (so we don't have to wait until a triggered job would normally fire)
app.shortcut('debug_manually_trigger_scheduled_jobs', async ({ ack, context, body }) => {
// Acknowledge right away
await ack()
Expand All @@ -248,9 +252,9 @@ app.error(error => {

(async () => {
// Schedule our dynamic cron jobs
scheduleReminders()
scheduleJobs()

// Actually start thhe Bolt app. Let's go!
// Actually start the Bolt app. Let's go!
await app.start(process.env.PORT || 3000)
console.log('⚡️ Bolt app is running!')
})()
3 changes: 2 additions & 1 deletion config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ const triageConfig = {
emoji: ':white_circle:'
}
},
scheduled_reminders: [
scheduled_jobs: [
{
expression: '0 * * * *',
type: 'triage',
hours_to_look_back: 24,
report_on_levels: ['Urgent', 'Medium'], // only report on messages with one of these levels ("OR" logic)
report_on_does_not_have_status: ['Acknowledged', 'Done'] // only report on messages that do not have either of these statuses ("OR")
Expand Down
8 changes: 4 additions & 4 deletions docs/DEPLOY_Heroku.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ In a new tab, do the following. Be sure to replace `awesome-app-name-you-entered
- Create a new Shortcut
- Select **Global** shortcut
- Choose a descriptive name and description for your shortcut, for example:
- Name: Show triage stats
- Name: Show channel stats
- Description: Calculate stats for a triage channel
- For the Callback ID, it is important you set it to `triage_stats`
- For the Callback ID, it is important you set it to `channel_stats`
- Enter your Select Menus Options Load URL `https://awesome-app-name-you-entered.herokuapp.com/slack/events`
- Click Save Changes

Expand Down Expand Up @@ -76,12 +76,12 @@ In a new tab, do the following. Be sure to replace `awesome-app-name-you-entered

2. Try out your freshly deployed app!
1. Visit your app's App Home tab to see the current configuration (you can edit `config.js` and restart the application to make changes)
2. Execute your shortcut by entering "Show triage stats" in the quick switcher (CMD+k) or by using the lightning bolt ⚡️ symbol right below the message input field in Slack and filling out the form. You should receive a DM from the bot.
2. Execute your shortcut by entering "Show channel stats" in the quick switcher (CMD+k) or by using the lightning bolt ⚡️ symbol right below the message input field in Slack and filling out the form. You should receive a DM from the bot.
3. Wait for the (by default) top-of-the-hour hourly update in any channel the bot has been invited to.

3. Take a moment to check out your Heroku addon.
- You should see that MongoLabs has some data in it
- Consider adding other addons to help you manage your app such as Logentries for ingesting your logs and NewRelic for monitoring performance characteristics.

4. Lastly, note that in the default configuration of this app, you should have one and only one web dyno running at a time as the scheduled reminder functionality runs _within_ the web application code courtesy of `node-cron`.
4. Lastly, note that in the default configuration of this app, you should have one and only one web dyno running at a time as the scheduled job functionality runs _within_ the web application code courtesy of `node-cron`.
- In production, you may want to disable this and outsource the scheduling to [Heroku Scheduler](https://devcenter.heroku.com/articles/scheduler) or another service/add-on.
8 changes: 4 additions & 4 deletions docs/SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ In your preferred web browser:
- Create a new Shortcut
- Select **Global** shortcut
- Choose a descriptive name and description for your shortcut, for example:
- Name: Show triage stats
- Name: Show channel stats
- Description: Calculate stats for a triage channel
- For the Callback ID, it is important you set it to `triage_stats`
- For the Callback ID, it is important you set it to `channel_stats`
- Enter your Select Menus Options Load URL `https://your-host/slack/events`


Expand Down Expand Up @@ -94,8 +94,8 @@ Back in your preferred web browser...

2. Try out your app!
1. Visit your app's App Home tab to see the current configuration (you can edit `config.js` and restart the application to make changes)
2. Execute your shortcut by entering "Show triage stats" in the quick switcher (CMD+k) or by using the lightning bolt ⚡️ symbol right below the message input field in Slack and filling out the form. You should receive a DM from the bot.
2. Execute your shortcut by entering "Show channel stats" in the quick switcher (CMD+k) or by using the lightning bolt ⚡️ symbol right below the message input field in Slack and filling out the form. You should receive a DM from the bot.
3. Wait for the (by default) top-of-the-hour hourly update in any channel the bot has been invited to.

3. Lastly, note that in the default configuration of this app, you should have one and only one Node process running at a time as the scheduled reminder functionality runs _within_ the web application code courtesy of `node-cron`.
3. Lastly, note that in the default configuration of this app, you should have one and only one Node process running at a time as the scheduled job functionality runs _within_ the web application code courtesy of `node-cron`.
- In production, you may want to disable this and outsource the scheduling to `crontab` or another schedule service/daemon.
Loading