telemetry-facade-pattern — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited telemetry-facade-pattern (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Telemetry targetTelemetry target inside the SwiftPM Package.TelemetryEvent value type (enum / struct, Sendable)TelemetrySink protocolTelemetry actor. Sink stateful subscriptions (e.g. MetricKitSink holding MXMetricManagerSubscriber reference identity) require an actor for clean lifecycle management. A Sendable struct facade is acceptable only when every sink is fully synchronous and stateless. The facade fans out to multiple sinks.telemetry.observe(.puzzleCompleted(id: puzzleId, durationMs: 12_345))| Sink | Receives | Purpose |
|---|---|---|
OSLogSink | All events | Human-readable debug messages |
TrackingSink (default NoOpTrackingSink) | Business events | v1 has no third-party tracking but the protocol is reserved; future swaps require zero call-site changes |
MetricKitSink | Subscribes via MXMetricManager.shared.add(self); on receiving MXMetricPayload, broadcasts to other sinks | Performance / diagnostics persistence |
GameCenterSink (games) | Completion / achievement events | Submit score / unlock achievement |
public struct NoOpTrackingSink: TelemetrySink {
public init() {}
public func receive(_ event: TelemetryEvent) { /* intentionally empty */ }
}#### Wiring traps (hard-won — real project lessons)
A sink that exists as a type is worth zero until it is in the live sinks array. Four traps, in the order they bit:
GameCenterSink/AchievementEvaluatorwere fully written but never added to the live Telemetry sinks list (the composition root shipped [OSLogSink, NoOpTrackingSink] only) → no score, no achievement, silently. Verify the composition root's actual sinks array, not that the sink type compiles. git log -S "GameCenterSink(" showing only the creation commit is the smoking gun.
forwards in array order, so a sink that writes state another sink reads must come first — e.g. PersonalRecordSink writes completedCount before GameCenterSink's evaluator reads it; reversed = an off-by-one where the count achievement fires one completion late. Make read/write sink order explicit and test it.
are reached from the interactive path (e.g. placeMove → sessionCompleted → telemetry.observe). A sink doing CloudKit reads + GameKit network I/O synchronously there freezes the UI. Forward to such sinks on a detached, order-preserving Task (chain each on the previous so events still forward in order) and return immediately; keep the fast sinks (OSLog / NoOp) synchronous.
(persistence, GameCenter) that themselves need Telemetry, you cannot build it at Telemetry-construction time. Wire a DeferredSink placeholder into the facade at startup, then setDownstream([real sinks]) once (sync, from the @MainActor composition root) after all deps are assembled. final class @unchecked Sendable + NSLock (not an actor) keeps setDownstream synchronous; receive snapshots state under the lock before any await.
uncovered a third gap: the GameKit terminal (submitScore/reportAchievement) was a stub that no-op'd / threw. Trace to the actual platform call (GKLeaderboard.submitScore, GKAchievement.report), not just to the sink. Terminal GameKit/StoreKit calls are device-gated — verify on a real device + sandbox, never claim "done" from a green headless suite.
telemetry.observe(...) with no external tracking, and a future TelemetryDeck / in-house pipeline only swaps the sink.Telemetry target and use Logger directly. But if you anticipate adding tracking / metrics later, building the facade up front pays off.MetricKitSink payloads must go through TrackingSink first): handle routing inside the facade; call sites still unchanged.Telemetry target is standalone; UI / Engine don't directly depend on anything beyond OSLog.TelemetryEvent is a value type, Sendable.NoOpTrackingSink is provided and wired in the composition root.intend to fire** (not just that the sink type exists) — the "existing-but-unwired" failure mode.
pinning the order.
interactive path is never frozen by a sink's CloudKit/GameKit work.
not just the sink.
oslog-logger-defaults: the concrete OSLogSink implementation dependency.apple-three-piece-analytics: each piece corresponds to one sink.swiftpm-modularization: why Telemetry is its own target.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.