Swift

Le paquet HonkMe est un client Swift 6.2 avec concurrence stricte, async/await et aucune dépendance. Il fonctionne sur macOS, iOS et Linux, pour Swift côté serveur, les outils et la CI.

Paquet
HonkMe Publié
Code source
github.com/honk-me/honk-swift · Licence MIT
Prérequis
Swift 6.2 ; macOS 14+, iOS 17+ ou Linux.

Installation

Dans Xcode : File ▸ Add Package Dependencies, puis collez 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"),

Une clé d’ingestion (honk_…) permet à quiconque la détient d’envoyer des messages dans son projet. Gardez-la sur vos serveurs, dans vos tâches et dans les secrets de votre CI, jamais dans un navigateur ni dans une app mobile ou de bureau.

Envoyer un événement

N’intégrez jamais de clé d’ingestion dans une app iOS ou macOS : tout ce qui est embarqué dans une app peut en être extrait. Si une app doit vous prévenir, faites-la passer par votre propre backend, qui appelle ensuite 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")

Recette : une demande client avec Vapor

Message est Sendable : vous pouvez le construire dans le handler et l’envoyer en dehors du traitement de la requête. Créez un seul Honk au démarrage et partagez-le : il possède une URLSession keep-alive.

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
    }
}

L’échelle Honk et les incidents

Les helpers acceptent une closure qui modifie le 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

Erreurs

Les cas de HonkError portent un Failure avec le statut, le code, chaque champ invalide et retryAfter ; isRetryable vous indique quoi faire.

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)
}

Des nouvelles tentatives sans doublon

  • Chaque envoi porte une Idempotency-Key : la vôtre, ou un UUIDv7 généré pour l’occasion. La même clé est réutilisée à chaque nouvelle tentative, et pendant 24 heures Honk répond à une requête rejouée avec l’identifiant d’origine et duplicate: true : une réponse perdue ne crée jamais de second message.
  • Une nouvelle tentative n’a lieu qu’en cas d’erreur réseau, de délai dépassé, de 429 ou de 5xx, avec un backoff exponentiel et un jitter complet, jamais avant le Retry-After du serveur. Les autres réponses 4xx ne donnent lieu à aucune nouvelle tentative : corrigez plutôt la requête.
  • Chaque tentative expire au bout de 5 secondes, et tout s’arrête au bout de 30 secondes au total. Une attente qui dépasserait cette échéance, comme un quota quotidien remis à zéro à minuit, échoue immédiatement et indique quand réessayer.
  • Les champs sont vérifiés localement avant l’envoi, et tous les champs invalides sont signalés en une seule fois.