| name | post-daily-new-issues |
| description | Post the daily digest of new Sentry issues for the current stable release to an on-call Slack channel. Use when the user asks to run or post the daily stable Sentry digest for on-call. |
post-daily-new-issues
Post a daily digest of new Sentry issues for the current release on $DIGEST_CHANNEL (default stable) to an on-call Slack channel, tagging the on-call usergroup. The digest is scoped to a single calendar date so a scheduled agent can run this once per weekday without re-reporting issues that were already shared on a previous day.
Configuration
This skill reads environment-specific values from environment variables (see the repo README.md):
SLACK_BOT_TOKEN — Slack bot token with channels:read, chat:write, usergroups:read scopes.
SENTRY_AUTH_TOKEN — Sentry auth token with event:read and project:read scopes. Used to call the Sentry REST API directly.
SENTRY_ORG — Sentry org slug.
SENTRY_PROJECT — Sentry project slug (e.g. your release client project).
SENTRY_PROJECT_ID — numeric Sentry project ID (used to build release URLs).
RELEASE_SLACK_CHANNEL — Slack channel name (or ID) to post the digest in.
ONCALL_SLACK_GROUP — Slack usergroup handle for the on-call (e.g. oncall-primary).
RELEASE_VERSIONS_URL — optional public URL returning a JSON document with the current release version. Omit if you pass a version explicitly.
DIGEST_CHANNEL — channel whose release the digest reports on (default stable); also selects the version key read from RELEASE_VERSIONS_URL.
Step 0: Decide whether to run today (weekend skip)
This skill is intentionally a no-op on Saturday and Sunday. Compute the day of week in UTC:
DOW=$(date -u +%u)
- If
DOW is 6 (Sat) or 7 (Sun): print a short note (e.g. "Skipping: weekend") and exit without posting to Slack.
- Otherwise continue.
If the caller passed an explicit target date (see Step 2), skip this step entirely and use what they provided.
Step 1: Find the current release version
If RELEASE_VERSIONS_URL is set, read the current version for $DIGEST_CHANNEL from it. The exact JSON path depends on your release metadata format; the channel key is selected by $DIGEST_CHANNEL, for example:
RELEASE_VERSION=$(curl -fsS "$RELEASE_VERSIONS_URL" | jq -r --arg ch "$DIGEST_CHANNEL" '.[$ch].version')
RELEASE_VERSION looks like v0.YYYY.MM.DD.HH.MM.stable_NN (a documented example template — adjust to match the version scheme your release pipeline emits). If RELEASE_VERSIONS_URL is unset, ask the user for the current release version. If the field is missing or curl fails, stop and surface the error — do not post a partial digest.
Step 2: Compute the target date window
The window is a half-open UTC interval [START_DATE, END_DATE) (i.e. firstSeen >= START_DATE AND firstSeen < END_DATE). Both are YYYY-MM-DD strings.
The default behavior depends on which weekday the agent is running:
- Tue–Fri (
DOW 2–5): report yesterday only.
START_DATE = yesterday (UTC), END_DATE = today (UTC).
- Mon (
DOW 1): report Saturday + Sunday + the previous Monday's overnight, because the agent did not run on the weekend.
START_DATE = 3 days ago (UTC, that Saturday), END_DATE = today (UTC, that Monday).
On macOS (BSD date):
TODAY=$(date -u +%Y-%m-%d)
if [ "$DOW" = "1" ]; then
START_DATE=$(date -u -v-3d +%Y-%m-%d)
else
START_DATE=$(date -u -v-1d +%Y-%m-%d)
fi
END_DATE=$TODAY
On Linux (GNU date, e.g. inside a remote environment):
TODAY=$(date -u +%Y-%m-%d)
if [ "$DOW" = "1" ]; then
START_DATE=$(date -u -d '3 days ago' +%Y-%m-%d)
else
START_DATE=$(date -u -d 'yesterday' +%Y-%m-%d)
fi
END_DATE=$TODAY
Override: if the user explicitly asks for a different window (e.g. "post the digest for 2026-04-25"), use their dates instead and skip the weekend check.
Step 3: Fetch new issues from Sentry
Call the Sentry REST API directly with $SENTRY_AUTH_TOKEN. The query string must include both:
firstRelease:<RELEASE_VERSION> — restricts to issues whose first-seen release matches the live release build (this matches Sentry's "New Issues" tab on the release page).
firstSeen:>=START_DATE firstSeen:<END_DATE — narrows to the target window so already-reported issues from previous days are excluded. This is the noise-control mechanism; do not omit it.
Sort by frequency (sort=freq) and request limit=100 so the true total and the top 3 are both stable even if there's a long tail of one-event issues.
QUERY="firstRelease:${RELEASE_VERSION} firstSeen:>=${START_DATE} firstSeen:<${END_DATE}"
ISSUES_JSON=$(curl -fsS -G "https://sentry.io/api/0/projects/${SENTRY_ORG}/${SENTRY_PROJECT}/issues/" \
-H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \
--data-urlencode "query=$QUERY" \
--data-urlencode "sort=freq" \
--data-urlencode "limit=100")
The response is a JSON array. Each element has at least: shortId, title, count (event count, as a string), userCount, firstSeen, lastSeen, permalink. Parse it with jq.
If the array is empty, exit silently — do NOT post anything to Slack. Print a short note like "No new issues in window; skipping post" so the run log makes the no-op visible, then stop. The on-call channel should only get a message when there's something to act on; an empty digest just adds noise.
Step 4: Compute the total and pick the top 3
The message has two distinct numbers, and they must come from different slices of the response:
TOTAL_NEW_ISSUES — the full count of new issues in the window (used in the "N new issues since …" summary line). Compute this from the entire array, before any truncation.
- Top 3 issues — the first 3 entries by event count, used to populate the bullet list. The list is intentionally short to keep the digest scannable; the on-call clicks through to Sentry for the long tail.
For each of the top 3, capture:
- Short ID from
.shortId
- Title from
.title — truncate to ~140 chars on a single line
- Event count from
.count (cast to int)
- User count from
.userCount
- Issue URL: prefer
.permalink if present, else build it as https://${SENTRY_ORG}.sentry.io/issues/<SHORT_ID>
If fewer than 3 issues exist, include all of them.
Example jq to compute both numbers in one pass:
TOTAL_NEW_ISSUES=$(printf '%s' "$ISSUES_JSON" | jq 'length')
BULLETS=$(printf '%s' "$ISSUES_JSON" | jq -r --arg org "$SENTRY_ORG" '
[.[] | {
short: .shortId,
title: (.title | gsub("\n"; " ") | .[0:140]),
events: (.count | tonumber),
users: .userCount,
url: (.permalink // ("https://" + $org + ".sentry.io/issues/" + .shortId))
}]
| sort_by(-.events)
| .[0:3]
| map("• <\(.url)|\(.short)> — \(.events) events, \(.users) users — \(.title)")
| join("\n")
')
Step 5: Resolve Slack IDs
Resolve once per run. If the channel ID and subteam ID are stable in your environment you may cache them, but always validate they still match.
CHANNEL_ID=$(curl -s -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
"https://slack.com/api/conversations.list?types=public_channel,private_channel&limit=1000" | \
jq -r --arg name "$RELEASE_SLACK_CHANNEL" '.channels[] | select(.name==$name) | .id')
SUBTEAM_ID=$(curl -s -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
"https://slack.com/api/usergroups.list" | \
jq -r --arg handle "$ONCALL_SLACK_GROUP" '.usergroups[] | select(.handle==$handle) | .id')
If either lookup is empty, surface the error and stop — do not guess IDs.
Step 6: Format and post the message
Use Slack Block Kit. The mention must be <!subteam^${SUBTEAM_ID}> so it actually pings the on-call group. Build a text fallback for notifications and search.
Build the Sentry release link first — every message must include it so the on-call can jump straight into the filtered "new issues" view:
RELEASE_URL="https://${SENTRY_ORG}.sentry.io/explore/releases/${RELEASE_VERSION}/?issuesType=new&project=${SENTRY_PROJECT_ID}"
The issuesType=new query param is what pre-selects the New Issues tab; do not omit it. project=${SENTRY_PROJECT_ID} is the numeric ID for your Sentry project.
Message structure:
- Header:
<RELEASE_VERSION>: new Sentry issues for <date or range>
- Section with the subteam mention and a one-line summary that uses the true total, not the bullet count. Example:
<!subteam^S012345> — ${TOTAL_NEW_ISSUES} new issues since <START_DATE> · <RELEASE_URL|View in Sentry>
On a Monday run, phrase the window as since Saturday instead of a bare date.
- Section listing only the top 3 issues by event count, one per line, in this format:
• <ISSUE_URL|SHORT_ID> — <events> events, <users> users — <title>
- Section with a triage reminder, e.g.:
_Please <${RELEASE_URL}|review the full New Issues tab in Sentry> and triage._
This is what tells the on-call the bullets are only a sample and that they need to click through.
Use Slack mrkdwn link syntax (<URL|text>) inside mrkdwn blocks. Keep titles on a single line; truncate with … if needed.
If TOTAL_NEW_ISSUES is zero, do not reach this step — the skill exits in Step 3 without posting. There is no "all clear" message; silence is the all-clear signal.
Date display:
- Tue–Fri: show
START_DATE (e.g. 2026-04-28 (UTC)).
- Mon: show the range, e.g.
Sat 2026-04-25 – Mon 2026-04-27 (UTC).
Post as a top-level message (no thread_ts). Suppress link previews so the message stays compact — with several Sentry links per post, unfurls would dominate the channel:
curl -s -X POST \
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
-H "Content-Type: application/json" \
--data "$(jq -n \
--arg channel "$CHANNEL_ID" \
--arg text "$TEXT_FALLBACK" \
--argjson blocks "$BLOCKS_JSON" \
'{channel: $channel, text: $text, blocks: $blocks, unfurl_links: false, unfurl_media: false}')" \
"https://slack.com/api/chat.postMessage" | jq '{ok, ts, error}'
unfurl_links: false blocks regular URL previews; unfurl_media: false blocks image/video previews. Both are required — omitting either lets Slack render whichever class of preview it didn't see suppressed.
If ok is false, log the error field and stop. Common failures: not_in_channel (invite the bot), missing_scope, channel_not_found.
Step 7: Confirm
Fetch and print the permalink so a human (or downstream agent) can click into the post:
curl -s -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
"https://slack.com/api/chat.getPermalink?channel=$CHANNEL_ID&message_ts=<MESSAGE_TS>" | \
jq -r '.permalink'
Notes
- Why scope by
firstSeen and not just firstRelease? Without the date filter, every daily run would re-post the same long-lived issues from earlier in the release's lifetime. Filtering by firstSeen on the target date is what makes the digest actionable and not noisy.
- Why bundle the weekend on Monday instead of running on Sat/Sun? If the on-call rotation is staffed Mon–Fri, weekend posts would go unread. Bundling on Monday surfaces anything that broke over the weekend in a single message.
- Manual reruns: if the user provides explicit
START_DATE / END_DATE (e.g. for a backfill or to retry a missed day), use those and skip the weekday/weekend logic.
- A reusable helper that fetches and normalizes issues is provided at
scripts/fetch_new_issues.sh.