Swift

The HonkMe package is a Swift 6.2 client with strict concurrency, async/await and no dependencies. It runs on macOS, iOS and Linux, for server-side Swift, tools and CI.

Package
HonkMe Published
Source
github.com/honk-me/honk-swift · MIT License
Requirements
Swift 6.2; macOS 14+, iOS 17+ or Linux.

Install

In Xcode: File ▸ Add Package Dependencies and paste https://github.com/honk-me/honk-swift.

Package.swift
// Package.swift
.package(url: "https://github.com/honk-me/honk-swift.git", from: "0.1.0"),
// target:
.product(name: "HonkMe", package: "honk-swift"),

Anyone with an ingestion key (honk_…) can post to its project. Keep it on servers, in jobs and in CI secrets, never in a browser, mobile or desktop app.

Send an event

Never embed an ingestion key in an iOS or macOS app: anything shipped inside an app can be extracted. An app that needs to notify you should call your own backend, which then calls Honk.

import HonkMe

let honk = try Honk.fromEnvironment()          // HONK_URL, HONK_KEY (+ HONK_SOURCE, HONK_ENVIRONMENT, HONK_CHANNEL)
try await honk.beep("Backup finished", "nightly pg_dump took 42 s")

Recipe: a customer request with Vapor

Message is Sendable, so you can build it in the handler and send it off the request path. Create one Honk at startup and share it: it owns a keep-alive URLSession.

import HonkMe
import Vapor

func routes(_ app: Application, honk: Honk) {
    app.post("quote") { req async throws -> HTTPStatus in
        let quote = try req.content.decode(QuoteRequest.self)
        try await quote.save(on: req.db)                               // store it first
        let id = try quote.requireID()
        // Message is Sendable: build it here, send it off the request path.
        let message = Message(
            "\(quote.name) (\(quote.company)) asked: \(quote.body.prefix(2000))",
            title: "New request: \(quote.subject)".prefix(150).description,
            severity: .light,
            priority: .high,                                           // push right away
            category: .customers,
            channel: "requests",
            groupKey: "requests/\(id)",                                // one group per request
            url: "https://shop.example.com/admin/requests/\(id)",      // https only
            metadata: ["request_id": .string("\(id)")]
        )
        let logger = req.logger
        Task {
            do { try await honk.send(message, idempotencyKey: "request-\(id)") }   // same request, same key
            catch { logger.warning("honk: \(error)") }
        }
        return .accepted
    }
}

The Honk scale and incidents

Helpers take a closure that edits the message.

try await honk.loud("Disk 91%", "/var on app-01") { $0.groupKey = "disk/app-01/var" }
try await honk.problem(groupKey: "db/backup", "Backup failed", "pg_dump exited with 1")   // a long honk by default
try await honk.recovery(groupKey: "db/backup", "Backup OK", "pg_dump finished in 41 s")  // a beep by default

Errors

HonkError cases carry a Failure with the status, the code, every invalid field and retryAfter; isRetryable tells you what to do.

do {
    try await honk.long("Payment failed", "Stripe declined order 1042") { $0.groupKey = "payments/stripe" }
} catch HonkError.validation(let failure) {
    logger.error("bug: \(failure.fields)")
} catch let error as HonkError where error.isRetryable {
    queue.retryLater(key: error.failure?.idempotencyKey)
}

Retries that never send twice

  • Every send carries an Idempotency-Key: yours, or a fresh UUIDv7. The same key is reused on every retry, and within 24 hours Honk answers a replay with the original id and duplicate: true, so a lost response never turns into a second message.
  • Only network errors, timeouts, 429 and 5xx are retried, with exponential backoff and full jitter, never sooner than the server’s Retry-After. Other 4xx responses are never retried; fix the request instead.
  • Each attempt times out after 5 seconds and everything stops after 30. A wait that would cross that deadline, like a daily quota that resets at midnight, fails right away and tells you when to retry.
  • Fields are validated before sending, and all invalid fields are reported together.