-
Notifications
You must be signed in to change notification settings - Fork 116
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
feat: ability to post slack upates #3744
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
0471f88
feat: ability to post slack upates
SiTaggart 3fd5384
chore: schema types update
SiTaggart fb70698
chore: make callable from any environment
SiTaggart 6d28825
chore: automate it with vercel cron jobs
SiTaggart a11d2c6
Merge branch 'main' into feat/create-slack-posts
SiTaggart c8e21ee
Merge branch 'main' into feat/create-slack-posts
SiTaggart File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
28 changes: 28 additions & 0 deletions
28
apps/backend/supabase/migrations/20240129215251_top_searches.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
set check_function_bodies = off; | ||
|
||
CREATE OR REPLACE FUNCTION public.match_weekly_search_usage() | ||
RETURNS TABLE(query_string character varying, count bigint) | ||
LANGUAGE plpgsql | ||
AS $function$ | ||
begin | ||
return query | ||
SELECT | ||
queries.query_string, | ||
count(*) as count | ||
FROM | ||
queries | ||
WHERE | ||
type = 'docs-search' | ||
AND queries.query_string != 'dsys' | ||
AND queries.query_string != 'create a button' | ||
AND queries.created_at >= now() - interval '1 week' | ||
GROUP BY | ||
queries.query_string | ||
ORDER BY | ||
count DESC | ||
LIMIT 20; | ||
end; | ||
$function$ | ||
; | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import { LogLevel, WebClient } from "@slack/web-api"; | ||
import type { NextApiRequest, NextApiResponse } from "next"; | ||
import Rollbar from "rollbar"; | ||
|
||
import { logger } from "../../functions-utils/logger"; | ||
|
||
const rollbar = new Rollbar({ | ||
accessToken: process.env.ROLLBAR_ACCESS_TOKEN, | ||
captureUncaught: true, | ||
captureUnhandledRejections: true, | ||
}); | ||
|
||
const slackToken = process.env.SLACK_BOT_USER_OAUTH_ACCESS_TOKEN; | ||
|
||
const client = new WebClient(slackToken, { | ||
// LogLevel can be imported and used to make debugging simpler | ||
logLevel: LogLevel.DEBUG, | ||
}); | ||
|
||
const LOG_PREFIX = "[/api/post-to-slack]:"; | ||
|
||
export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void | Response> { | ||
logger.info(`${LOG_PREFIX} Incoming request`); | ||
|
||
if (slackToken === undefined) { | ||
logger.error(`${LOG_PREFIX} Slack token is undefined`); | ||
rollbar.error(`${LOG_PREFIX} Slack token is undefined`); | ||
res.status(500).json({ | ||
error: "Slack token is undefined", | ||
}); | ||
return; | ||
} | ||
|
||
const requestData = typeof req.body === "string" ? JSON.parse(req.body) : req.body; | ||
logger.info(`${LOG_PREFIX} Request data`, { requestData }); | ||
|
||
if (!requestData) { | ||
res.status(500).json({ | ||
error: "No request data", | ||
}); | ||
} | ||
|
||
// thread is optional if you want to reply to a Slack thread | ||
const { message, channelID, threadID } = requestData; | ||
logger.info(`${LOG_PREFIX} User query`, { message, channelID, threadID }); | ||
|
||
if (message === undefined || message === "") { | ||
logger.error(`${LOG_PREFIX} Message is undefined`); | ||
rollbar.error(`${LOG_PREFIX} Message is undefined`); | ||
res.status(500).json({ | ||
error: "Message is undefined", | ||
}); | ||
return; | ||
} | ||
|
||
if (channelID === undefined || channelID === "") { | ||
logger.error(`${LOG_PREFIX} Channel is undefined`); | ||
rollbar.error(`${LOG_PREFIX} Channel is undefined`); | ||
res.status(500).json({ | ||
error: "Channel is undefined", | ||
}); | ||
return; | ||
} | ||
|
||
try { | ||
const result = await client.chat.postMessage({ | ||
channel: channelID, | ||
text: message, | ||
username: "Paste Bot", | ||
// eslint-disable-next-line camelcase | ||
thread_ts: threadID, | ||
mrkdwn: true, | ||
}); | ||
|
||
logger.info(`${LOG_PREFIX} Request data`, { result }); | ||
res.status(200).json({ result }); | ||
} catch (error) { | ||
logger.error(`${LOG_PREFIX} Error posting to Slack`, { error }); | ||
rollbar.error(`${LOG_PREFIX} Error posting to Slack`, { error }); | ||
res.status(500).json({ | ||
error, | ||
}); | ||
} | ||
} |
157 changes: 157 additions & 0 deletions
157
packages/paste-website/src/pages/api/share-search-usage.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
import { createClient } from "@supabase/supabase-js"; | ||
import type { NextApiRequest, NextApiResponse } from "next"; | ||
import Rollbar from "rollbar"; | ||
import { z } from "zod"; | ||
|
||
import { logger } from "../../functions-utils/logger"; | ||
|
||
const queryCountSchema = z.object({ | ||
// eslint-disable-next-line camelcase | ||
query_string: z.string(), | ||
count: z.number(), | ||
}); | ||
|
||
const latestQueriesDataSchema = z.array(queryCountSchema); | ||
|
||
const rollbar = new Rollbar({ | ||
accessToken: process.env.ROLLBAR_ACCESS_TOKEN, | ||
captureUncaught: true, | ||
captureUnhandledRejections: true, | ||
}); | ||
|
||
const supabaseUrl = process.env.SUPABASE_URL; | ||
const supabaseServiceKey = process.env.SUPABASE_KEY; | ||
const slackChannelID = process.env.SLACK_CHANNEL_HELP_DESIGN_SYSTEM; | ||
|
||
const LOG_PREFIX = "[/api/share-search-usage]:"; | ||
|
||
export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void | Response> { | ||
logger.info(`${LOG_PREFIX} Incoming request`); | ||
|
||
// if this doesn't get called from a vercel cron job, then it's unauthorized | ||
if (req.headers.authorization !== `Bearer ${process.env.CRON_SECRET}`) { | ||
res.status(401).end("Unauthorized"); | ||
return; | ||
} | ||
|
||
if (supabaseUrl === undefined) { | ||
logger.error(`${LOG_PREFIX} Supabase URL is undefined`); | ||
rollbar.error(`${LOG_PREFIX} Supabase URL is undefined`); | ||
res.status(500).json({ | ||
error: "Supabase URL is undefined", | ||
}); | ||
return; | ||
} | ||
|
||
if (supabaseServiceKey === undefined) { | ||
logger.error(`${LOG_PREFIX} Supabase service key is undefined`); | ||
rollbar.error(`${LOG_PREFIX} Supabase service key is undefined`); | ||
res.status(500).json({ | ||
error: "Supabase service key is undefined", | ||
}); | ||
return; | ||
} | ||
|
||
if (slackChannelID === undefined) { | ||
logger.error(`${LOG_PREFIX} Slack channel ID is undefined`); | ||
rollbar.error(`${LOG_PREFIX} Slack channel ID is undefined`); | ||
res.status(500).json({ | ||
error: "Slack channel ID is undefined", | ||
}); | ||
return; | ||
} | ||
|
||
const { host } = req.headers; | ||
const protocol = host?.includes("localhost") ? "http" : "https"; | ||
|
||
const supabaseClient = createClient(supabaseUrl, supabaseServiceKey); | ||
|
||
const latestSearchQueriesResponse = await supabaseClient.rpc("match_weekly_search_usage"); | ||
|
||
if (latestSearchQueriesResponse.error) { | ||
logger.error(`${LOG_PREFIX} Error fetching latest search queries`, { | ||
latestSearchQueries: latestSearchQueriesResponse, | ||
}); | ||
rollbar.error(`${LOG_PREFIX} Error fetching latest search queries`, { | ||
latestSearchQueries: latestSearchQueriesResponse, | ||
}); | ||
res.status(500).json({ | ||
error: latestSearchQueriesResponse.error, | ||
}); | ||
return; | ||
} | ||
|
||
const latestSearchQueries = latestQueriesDataSchema.safeParse(latestSearchQueriesResponse.data); | ||
|
||
logger.info(`${LOG_PREFIX} Latest search queries`, { latestSearchQueriesData: latestSearchQueries }); | ||
|
||
if (!latestSearchQueries.success) { | ||
logger.error( | ||
`${LOG_PREFIX} Invalid db query response structure: ${JSON.stringify(latestSearchQueriesResponse.data)}`, | ||
); | ||
rollbar.error( | ||
`${LOG_PREFIX} Invalid db query response structure: ${JSON.stringify(latestSearchQueriesResponse.data)}`, | ||
); | ||
res.status(400).json({ error: "Invalid db query response structure", details: latestSearchQueries.error }); | ||
return; | ||
} | ||
|
||
/** | ||
* Take this structured data and turn it into a markdown table | ||
* [ | ||
* { | ||
* "query_string": "button", | ||
* "count": 2 | ||
* } | ||
* ] | ||
*/ | ||
|
||
const queries = latestSearchQueries.data.map((query) => query.query_string); | ||
const counts = latestSearchQueries.data.map((query) => query.count.toString()); | ||
|
||
const maxQueryLength = Math.max(...queries.map((query) => query.length), "Query".length); | ||
const maxCountLength = Math.max(...counts.map((count) => count.length), "Count".length); | ||
|
||
let markdownTable = `| ${"Search Term".padEnd(maxQueryLength)} | ${"Count".padEnd(maxCountLength)} |\n| ${"-".repeat( | ||
maxQueryLength, | ||
)} | ${"-".repeat(maxCountLength)} |\n`; | ||
|
||
for (const query of latestSearchQueries.data) { | ||
markdownTable += `| ${query.query_string.padEnd(maxQueryLength)} | ${query.count | ||
.toString() | ||
.padEnd(maxCountLength)} |\n`; | ||
} | ||
|
||
logger.info(`${LOG_PREFIX} Markdown table`, { markdownTable }); | ||
|
||
const slackMessage = | ||
`:mag: *Top Search Queries of the Past Week on Paste Docsite* :mag:\n\nThe search feature on the PasteDoc site is a great resource when you're stuck. Check out what your Paste Mates have been searching for most over the past 7 days.\n\n\`\`\`\n${markdownTable}`.concat( | ||
"\n```", | ||
); | ||
|
||
try { | ||
const postToSlackResponse = await fetch(`${protocol}://${host}/api/post-to-slack`, { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
body: JSON.stringify({ | ||
message: slackMessage, | ||
channelID: slackChannelID, | ||
}), | ||
}); | ||
|
||
logger.info(`${LOG_PREFIX} Posted to Slack`, { postToSlackResponse }); | ||
// return the data to the client | ||
res.status(200).json({ | ||
latestSearchQueriesData: latestSearchQueries, | ||
}); | ||
} catch (error) { | ||
logger.error(`${LOG_PREFIX} Error posting to Slack`, { error }); | ||
rollbar.error(`${LOG_PREFIX} Error posting to Slack`, { error }); | ||
res.status(500).json({ | ||
error: "Error posting to Slack", | ||
details: error, | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"crons": [ | ||
{ | ||
"path": "/api/share-search-usage", | ||
"schedule": "0 10 * * 1" | ||
} | ||
] | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: I almost wish logger.error also called rollbar.error internally. Would remove a lot of this duplication code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's just a instantiation of a winston logger, we might be able to create a custom transporter that calls rollbar on error maybe? No sure if that's its intended purpose though.