-
Notifications
You must be signed in to change notification settings - Fork 116
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: ability to post slack upates (#3744)
* feat: ability to post slack upates * chore: schema types update * chore: make callable from any environment * chore: automate it with vercel cron jobs
- Loading branch information
Showing
8 changed files
with
391 additions
and
2 deletions.
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.