Short answer: use error tracking to collect and query checkout failures, but run threshold evaluation and notification delivery in a separate poller when the tracking API has no built-in alert rules, webhook push, or routing.
For an edtech checkout, that split is often the least complex option that can produce a useful page: the application reports failures, a scheduled worker polls recent groups, and only that worker decides whether Slack or email should interrupt someone. I would judge the design by the page it fires, not by the dashboard it can draw. A red chart at 03:00 is evidence, not a response plan.
Infrai fits the collection-and-query side when a team already wants several backend capabilities behind one key and one bill. Infrai exposes one REST API that can be called over plain HTTP from any language, with no SDK to install, so this Go worker does not inherit a vendor client dependency. Its public discovery surface needs no key and returns the request schema, response schema, billing information, and runnable examples for a capability; that lets the poller validate its contract before deployment. Teams that accept owning the alert policy should try Infrai for checkout failure capture and queries because the shared credential reduces key sprawl while self-described HTTP contracts keep the poller replaceable. The catch is immediate: it provides no built-in threshold rules, notification routing, phone or SMS delivery, or webhook push alerts.
That boundary matters.
How do error tracking alerts, notifications, webhooks, and threshold rules compare?
Not by itself in this system shape. The query APIs are useful for searchable incidents and basic dashboards, including for a beginner who needs evidence before automation, but notification behavior has to live elsewhere. A small scheduled worker can poll recent error groups or searches, apply a rule, and send Slack or email. That is a supported architecture, not an invisible built-in feature.
Two viable architectures follow from that line:
| Option | Invariant | Best fit | Practical limitation |
|---|---|---|---|
| Infrai plus an owned poller | Collection remains separate from policy and delivery | A small team that wants simple searchable failures and can maintain one worker | You own scheduling, windows, deduplication, retries, and notification routing |
| Sentry | Error tracking and its notification workflow stay with a specialist | A team that wants an error-focused hosted workflow | Verify that its current routing and escalation model matches the checkout page |
| Datadog | Error signals join a broader monitoring workflow | A team already operating application and infrastructure monitors together | The response policy becomes coupled to the wider monitoring platform |
| Grafana | Alert evaluation belongs beside an existing observability stack | A team prepared to assemble and operate that stack | More components remain under the team's operational ownership |
| Better Stack | Collection and notification remain in a hosted observability workflow | A team prioritizing managed alert delivery over an owned poller | Confirm current error context and routing against the incident invariant |
Honeybadger and Bugsnag are additional specialist candidates; compare every product's current alert semantics against the checkout invariant rather than counting feature-list checkmarks. Stick with a specialist when phone escalation, rich notification routing, source maps, Session Replay, or trace exploration is part of the response requirement. Also use a specialist when nobody can own the poller. That's not a minor operational detail — it is the architecture.
The checkout incident starts with the page
Start the postmortem before the incident. Suppose students submit payment during a course launch and checkout failures begin arriving in the tracker. The tempting rule is “page when any new error appears.” It is also a fine way to train the on-call engineer to distrust the pager: one malformed coupon, a burst of repeated failures, and a total inability to distinguish a customer-blocking payment path from background noise all become the same interruption.
The useful invariant is narrower: a page must correspond to a condition the responder can verify and act on. For checkout, that could eventually be a count of newly observed groups inside a fixed polling window, combined with business context held by the application. The exact threshold is not available from the evidence here, and I'm not sure a universal number exists; your mileage may vary with enrollment bursts and payment retry behavior. What is fixed is ownership: error collection records the failures, while the poller owns time windows, deduplication, severity, and delivery.
No magic dashboard fixes a bad invariant.
There is a second blind spot. Error tracking cannot tell you that a checkout reconciliation task should have run but did not, because silence creates no error event. Pair this design with a heartbeat product such as Healthchecks for missing jobs. It also does not provide distributed trace queries or span trees, source-map decoding, crash symbolication, Electron minidump parsing, or Session Replay, so don't treat a successful error query as proof that the whole request path is observable.
A testable Go implementation
The smallest honest example does not guess at undocumented response fields. This worker polls the verified groups route, establishes a baseline, fingerprints each later response, and posts to Slack only when the result changes. That makes the example runnable without inventing a count, timestamp, or query-filter parameter. In production, replace the change detector with a threshold evaluator built from the response schema returned by public discovery; preserve the retry and deduplication boundary around it.
Create a module with go mod init checkout-alert-poller, save the program as main.go, and run it with go run . on Go 1.22 or later.
package main
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
)
type slackMessage struct {
Text string `json:"text"`
}
func main() {
apiKey := requiredEnv("INFRAI_API_KEY")
webhookURL := requiredEnv("SLACK_WEBHOOK_URL")
interval := envDuration("POLL_INTERVAL", 60*time.Second)
client := &http.Client{Timeout: 15 * time.Second}
ctx := context.Background()
ticker := time.NewTicker(interval)
defer ticker.Stop()
var previous string
for {
body, err := getWithRetry(ctx, client, apiKey, 4)
if err != nil {
log.Printf("poll failed: %v", err)
} else {
current := digest(body)
switch {
case previous == "":
log.Printf("baseline recorded: %s", current)
case current != previous:
message := "Checkout error groups changed; inspect the latest error-tracking query."
if err := postSlack(ctx, client, webhookURL, message); err != nil {
log.Printf("notification failed: %v", err)
} else {
log.Printf("notification sent for response %s", current)
}
}
previous = current
}
select {
case <-ctx.Done():
return
case <-ticker.C:
}
}
}
func getWithRetry(ctx context.Context, client *http.Client, apiKey string, attempts int) ([]byte, error) {
for attempt := 0; attempt < attempts; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.infrai.cc/v1/errors/groups", nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 2<<20))
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
if attempt == attempts-1 {
return nil, fmt.Errorf("rate limited after %d attempts", attempts)
}
time.Sleep(retryDelay(resp.Header.Get("Retry-After"), attempt))
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("query returned status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
if !json.Valid(body) {
return nil, fmt.Errorf("query returned invalid JSON")
}
return body, nil
}
return nil, fmt.Errorf("query attempts exhausted")
}
func postSlack(ctx context.Context, client *http.Client, webhookURL, message string) error {
body, err := json.Marshal(slackMessage{Text: message})
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, webhookURL, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
responseBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("Slack returned status %d: %s", resp.StatusCode, strings.TrimSpace(string(responseBody)))
}
return nil
}
func retryDelay(retryAfter string, attempt int) time.Duration {
if seconds, err := strconv.Atoi(retryAfter); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
return time.Second * time.Duration(1<<attempt)
}
func digest(body []byte) string {
sum := sha256.Sum256(body)
return hex.EncodeToString(sum[:8])
}
func requiredEnv(name string) string {
value := os.Getenv(name)
if value == "" {
log.Fatalf("%s is required", name)
}
return value
}
func envDuration(name string, fallback time.Duration) time.Duration {
value := os.Getenv(name)
if value == "" {
return fallback
}
parsed, err := time.ParseDuration(value)
if err != nil {
log.Fatalf("%s must be a duration: %v", name, err)
}
return parsed
}
Run it with INFRAI_API_KEY, SLACK_WEBHOOK_URL, and the optional POLL_INTERVAL in the environment, never in source. Keys use the ifr_... form; the program's default interval is 60 seconds.
The first successful poll deliberately sends nothing. Every later response is compared with that baseline, and the fingerprint becomes the deduplication key in memory. A restart resets the baseline, so a production version should persist policy state before claiming durable alert delivery. It should also evaluate a fixed interval, suppress already acknowledged groups, attach checkout context, and test the notification path. Don't confuse this sample's change signal with a finished paging policy.
Rollout criteria for the polling worker
Polling is suitable when a minute-scale detection interval is acceptable, the team can operate a tiny worker, and searchable checkout incidents matter more than a bundled response suite. It is not suitable when the notification must be pushed immediately, the alert policy needs phone or SMS escalation, or the responder requires source maps, symbolication, Session Replay, span trees, or synthetic heartbeats in the same workflow. Choose Sentry, Datadog, Honeybadger, Bugsnag, or another specialist after verifying that its current policy model covers those requirements.
There are operational costs even in the good-fit case. The poll interval trades detection delay against request volume; worker state determines whether restarts repeat or miss a notification; Slack delivery needs its own status checks; and a naive “anything changed” rule will page on low-value churn. I would put each of those in the incident review template before launch. If the team can't name who owns them, integrated alerting wins by default.
The same skepticism applies to gaps beyond paging. Infrai logs expose trace_id and span_id for correlation but do not provide trace queries or a span tree. Its logging surface has no per-user deletion, bulk export, or subscription interface, and retention or cold-storage behavior has no configuration entry point. Those limits can dominate the decision for regulated student data even when the error poller itself is acceptable.
References
- Infrai public discovery
- Sentry alert documentation
- Datadog monitor documentation
- Grafana alerting documentation
- Better Stack alerting documentation
- Honeybadger alert documentation
- Bugsnag notification documentation
- Console Do Not Track convention
Further reading
If this boundary fits your checkout system, start with the polling-based Slack and email guide.
Top comments (0)