agentsclimarketplace

Telemetry facade pattern

Skill wei18/apple-dev-skills/apple-dev-skills/skills/telemetry-facade-pattern

Reusable Claude Code skills for AI-agent-driven Swift / Apple-platform development — composable via git submodule; aggregates other specialist skill repos

Install
npx -y skills add wei18/apple-dev-skills --skill telemetry-facade-pattern

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Single `Telemetry` SwiftPM target with a fan-out facade — callers say "what happened" (`telemetry.observe(event)`), facade dispatches to multiple sinks (OSLog / NoOp tracking / MetricKit / Game Center). Invoke when starting a new project that will log + track, deciding logger / tracker coupling, designing telemetry interfaces, or when asked "should Logger and Tracking be one thing".

SKILL.md

7.0 KB, ~1.6k tokens by cl100k_base, as published. Nobody here has run it

Telemetry Facade Pattern

When to invoke

  • Starting a new project and designing the logger / tracker / metrics interface.
  • About to introduce OSLog and any tracking / analytics at the same time.
  • Wanting to preserve flexibility for "swap the tracking provider later".
  • User asks "should Logger and Tracking be separate", "how should the event interface look".

Default decisions

A single Telemetry target

  • Create one Telemetry target inside the SwiftPM Package.
  • It contains:
    • TelemetryEvent value type (enum / struct, Sendable)
    • TelemetrySink protocol
    • The main facade — default to a Telemetry 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.
    • Default sinks (see below)

Call sites describe only "what happened"

telemetry.observe(.puzzleCompleted(id: puzzleId, durationMs: 12_345))
  • The call site doesn't know who will consume the event.
  • Swapping providers / adding sinks only requires replacing a sink; call sites change nothing.

Default sink set

SinkReceivesPurpose
OSLogSinkAll eventsHuman-readable debug messages
TrackingSink (default NoOpTrackingSink)Business eventsv1 has no third-party tracking but the protocol is reserved; future swaps require zero call-site changes
MetricKitSinkSubscribes via MXMetricManager.shared.add(self); on receiving MXMetricPayload, broadcasts to other sinksPerformance / diagnostics persistence
GameCenterSink (games)Completion / achievement eventsSubmit score / unlock achievement
public struct NoOpTrackingSink: TelemetrySink {
    public init() {}
    public func receive(_ event: TelemetryEvent) { /* intentionally empty */ }
}

Composition root wiring

  • The App target's DI composition root injects sinks into the facade.
  • Sinks are independent; one sink's failure does not affect the others.

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:

  1. Existing-but-unwired = dead code. Real-world example: a GameCenterSink/AchievementEvaluator were 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.
  2. Sink ordering matters when one sink reads another's write. The facade 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.
  3. I/O sinks on a gameplay-reachable path must not block. Completion events 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.
  4. Late-binding to break the construction cycle. When a sink needs deps (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.
  5. The sink firing ≠ the terminal call working. Tracing "wire 2 things" 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.

Rationale

  • Decouples call sites from consumers: v1 can use telemetry.observe(...) with no external tracking, and a future TelemetryDeck / in-house pipeline only swaps the sink.
  • OSLog + Tracking + MetricKit + GameCenter are all "event streams"; one unified interface is easier to maintain than four separate ones.
  • Easy to test: inject a fake sink and assert on the event stream.

Deviation considerations

  • Minimal App, OSLog only: you can skip the Telemetry target and use Logger directly. But if you anticipate adding tracking / metrics later, building the facade up front pays off.
  • Need inter-sink dependencies (e.g. MetricKitSink payloads must go through TrackingSink first): handle routing inside the facade; call sites still unchanged.
  • Cross-platform (Android / Linux): facade interface stays platform-neutral; sink implementations are per-platform.

Verification checklist

  • The Telemetry target is standalone; UI / Engine don't directly depend on anything beyond OSLog.
  • TelemetryEvent is a value type, Sendable.
  • A default NoOpTrackingSink is provided and wired in the composition root.
  • Tests assert on event streams via fake sinks, not by parsing OSLog output.
  • The live composition root's sinks array actually contains every sink you intend to fire (not just that the sink type exists) — the "existing-but-unwired" failure mode.
  • Read/write-dependent sinks are ordered so writers precede readers, with a test pinning the order.
  • I/O sinks on a gameplay-reachable completion path forward non-blocking; the interactive path is never frozen by a sink's CloudKit/GameKit work.
  • The terminal platform call (GameKit/StoreKit) is reached and device-verified — not just the sink.

Related skills

  • 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.

Gives 0 of the 12 instructions most monitoring observability skills give in ~1.6k tokens

Counted across 481 of the 483 authors here whose files we hold, read 2026-08-06

  • link every alert to a runbookin 43 of 481, across 35 files
  • use structured json loggingin 36 of 481, across 31 files
  • alert on user-facing symptomsin 20 of 481, across 15 files
  • emit structured JSON logs with stable event namesin 18 of 481, across 13 files
  • propagate trace context across boundariesin 16 of 481
  • use histograms for latency trackingin 14 of 481, across 9 files
  • use OpenTelemetry for distributed tracingin 13 of 481, across 8 files
  • include a correlation ID on every log linein 13 of 481, across 8 files
  • Define service level objectivesin 10 of 481, across 7 files
  • Call useAzureMonitor before importing other modulesin 9 of 481, across 2 files
  • stop and ask for clarification if inputs are missingin 9 of 481, across 2 files
  • define on-call questions before adding telemetryin 9 of 481, across 4 files

Said here and by no other author read

  • create one Telemetry target
  • use an actor for the main facade
  • describe only what happened at call sites
  • default to a NoOp tracking sink
  • inject sinks at the composition root
  • isolate sinks from each other

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.