
Analytics Setup
- 2 installs
- 591 repo stars
- Updated July 24, 2026
- rshankras/claude-code-apple-skills
Generates protocol-based analytics infrastructure with swappable providers like TelemetryDeck, Firebase, and Mixpanel so telemetry providers can be changed in one line.
About
Generates a protocol-based analytics layer that lets an app track events while swapping between TelemetryDeck, Firebase, or Mixpanel without touching app code. A developer uses it to add telemetry with minimal provider lock-in.
- Protocol architecture swaps providers by changing one line
- Detects existing TelemetryDeck/Firebase/Mixpanel setups before generating
Analytics Setup by the numbers
- 2 all-time installs (skills.sh)
- Ranked #888 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rshankras/claude-code-apple-skills --skill analytics-setupAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 591 |
| Last updated | July 24, 2026 |
| Repository | rshankras/claude-code-apple-skills ↗ |
What it does
Generates protocol-based analytics infrastructure with swappable providers like TelemetryDeck, Firebase, and Mixpanel so telemetry providers can be changed in one line.
Files
Analytics Setup Generator
Generate a protocol-based analytics infrastructure that makes it easy to swap providers without changing app code.
When This Skill Activates
Use this skill when the user:
- Asks to "add analytics" or "set up analytics"
- Mentions "TelemetryDeck", "Firebase Analytics", "Mixpanel"
- Wants to "track events" or "add telemetry"
- Asks about "privacy-friendly analytics"
- Wants to swap analytics providers
Key Feature: Swappable Providers
The generated code uses a protocol-based architecture:
// Your app uses the protocol
analytics.track(.buttonTapped("subscribe"))
// Swap providers by changing ONE line:
let analytics: AnalyticsService = TelemetryDeckAnalytics() // or FirebaseAnalytics()Pre-Generation Checks
1. Project Context Detection
- [ ] Check for existing analytics implementations
- [ ] Look for TelemetryDeck/Firebase/Mixpanel in Package.swift or Podfile
- [ ] Identify source file locations
2. Conflict Detection
Search for existing analytics:
Glob: **/*Analytics*.swift, **/*Telemetry*.swift
Grep: "protocol.*Analytics" or "TelemetryDeck" or "Firebase"If found, ask user:
- Extend existing analytics?
- Replace with new implementation?
- Add new provider to existing setup?
Configuration Questions
Ask user via AskUserQuestion:
1. Which provider(s)?
- TelemetryDeck (privacy-friendly, recommended)
- Firebase Analytics
- Mixpanel
- None (NoOp for now, add later)
2. What events to track?
- App lifecycle (launch, background, foreground)
- Screen views
- User actions (buttons, features used)
- Errors
- Custom events
3. User properties?
- App version
- Subscription status
- Custom properties
Generation Process
Step 1: Create Core Files
Always generate these files: 1. AnalyticsService.swift - Protocol (never changes) 2. AnalyticsEvent.swift - Event definitions (app-specific) 3. NoOpAnalytics.swift - For testing/privacy mode
Step 2: Create Selected Provider(s)
Based on user selection:
TelemetryDeckAnalytics.swiftFirebaseAnalytics.swiftMixpanelAnalytics.swift
Step 3: Create Environment Integration
For SwiftUI apps:
AnalyticsServiceKey.swift- Environment key for dependency injection
Step 4: Determine File Location
Check project structure:
- If
Sources/exists →Sources/Analytics/ - If
App/exists →App/Analytics/ - Otherwise →
Analytics/
Output Format
After generation, provide:
Files Created
Sources/Analytics/
├── AnalyticsService.swift # Protocol (stable interface)
├── AnalyticsEvent.swift # Your app's events
├── Providers/
│ ├── NoOpAnalytics.swift # Testing/privacy
│ └── [Provider]Analytics.swift # Selected provider(s)
└── AnalyticsServiceKey.swift # SwiftUI Environment (optional)Integration Steps
App Entry Point:
@main
struct MyApp: App {
// Choose your provider
private let analytics: AnalyticsService = TelemetryDeckAnalytics(appID: "YOUR-APP-ID")
init() {
analytics.configure()
}
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.analytics, analytics)
}
}
}Tracking Events:
struct ContentView: View {
@Environment(\.analytics) private var analytics
var body: some View {
Button("Subscribe") {
analytics.track(.buttonTapped("subscribe"))
}
}
}Required Dependencies
TelemetryDeck:
// Package.swift
.package(url: "https://github.com/TelemetryDeck/SwiftClient", from: "1.0.0")Firebase:
// Package.swift
.package(url: "https://github.com/firebase/firebase-ios-sdk", from: "10.0.0")
// Also requires GoogleService-Info.plistSwapping Providers Later
To switch providers: 1. Add new provider file (or generate with this skill) 2. Change ONE line in App.swift:
// Before
private let analytics: AnalyticsService = TelemetryDeckAnalytics(...)
// After
private let analytics: AnalyticsService = FirebaseAnalytics()Testing
- Use
NoOpAnalytics()in tests and previews - All tracking calls become no-ops
- No external dependencies in tests
References
- analytics-patterns.md - Protocol architecture and best practices
- templates/ - All template files
Analytics Patterns and Best Practices
Protocol-Based Architecture
Why Protocols?
1. Testability - Use NoOpAnalytics in tests 2. Flexibility - Swap providers without code changes 3. Privacy - Easy to disable analytics per user preference 4. Gradual Migration - Add new provider, switch when ready
The Core Protocol
protocol AnalyticsService: Sendable {
func configure()
func track(_ event: AnalyticsEvent)
func track(_ event: AnalyticsEvent, properties: [String: String])
func setUserProperty(_ key: String, value: String)
func setUserID(_ id: String?)
func reset()
}Default Implementations
Provide defaults for optional methods:
extension AnalyticsService {
func track(_ event: AnalyticsEvent) {
track(event, properties: [:])
}
func setUserID(_ id: String?) {
// Optional - not all providers support this
}
func reset() {
// Called on logout - reset user properties
}
}Event Design
Enum-Based Events
Use enums for type-safe event tracking:
enum AnalyticsEvent: Sendable {
// Lifecycle
case appLaunched
case appBackgrounded
case appForegrounded
// Screens
case screenViewed(name: String)
// User Actions
case buttonTapped(name: String)
case featureUsed(name: String)
// Errors
case errorOccurred(domain: String, code: Int)
// Custom
case custom(name: String, properties: [String: String])
}Event Naming Conventions
| Pattern | Example | Notes |
|---|---|---|
noun_verbed | button_tapped | Past tense for completed actions |
screen_viewed | settings_screen_viewed | For navigation |
feature_used | dark_mode_enabled | For feature adoption |
Event Properties
Add context without bloating the enum:
// Good - properties in track call
analytics.track(.buttonTapped("subscribe"), properties: [
"screen": "paywall",
"variant": "annual"
])
// Avoid - too many enum cases
case subscribeButtonTappedOnPaywallAnnual // Don't do thisProvider Implementations
TelemetryDeck (Recommended)
Privacy-friendly, no user consent required in most jurisdictions:
final class TelemetryDeckAnalytics: AnalyticsService {
private let appID: String
init(appID: String) {
self.appID = appID
}
func configure() {
TelemetryManager.initialize(with: .init(appID: appID))
}
func track(_ event: AnalyticsEvent, properties: [String: String]) {
var allProperties = properties
allProperties["event_name"] = event.name
TelemetryManager.send(event.signalName, with: allProperties)
}
}Firebase Analytics
Full-featured but requires consent in EU:
final class FirebaseAnalytics: AnalyticsService {
func configure() {
FirebaseApp.configure()
}
func track(_ event: AnalyticsEvent, properties: [String: String]) {
Analytics.logEvent(event.firebaseName, parameters: properties)
}
func setUserProperty(_ key: String, value: String) {
Analytics.setUserProperty(value, forName: key)
}
func setUserID(_ id: String?) {
Analytics.setUserID(id)
}
}NoOp Implementation
Essential for testing and privacy:
final class NoOpAnalytics: AnalyticsService {
func configure() {}
func track(_ event: AnalyticsEvent, properties: [String: String]) {}
func setUserProperty(_ key: String, value: String) {}
func setUserID(_ id: String?) {}
func reset() {}
}SwiftUI Integration
Environment Key
private struct AnalyticsServiceKey: EnvironmentKey {
static let defaultValue: AnalyticsService = NoOpAnalytics()
}
extension EnvironmentValues {
var analytics: AnalyticsService {
get { self[AnalyticsServiceKey.self] }
set { self[AnalyticsServiceKey.self] = newValue }
}
}Usage in Views
struct SettingsView: View {
@Environment(\.analytics) private var analytics
var body: some View {
List {
Button("Reset") {
analytics.track(.buttonTapped("reset_settings"))
// ... reset logic
}
}
.onAppear {
analytics.track(.screenViewed(name: "Settings"))
}
}
}View Modifier for Screen Tracking
struct AnalyticsScreenModifier: ViewModifier {
let screenName: String
@Environment(\.analytics) private var analytics
func body(content: Content) -> some View {
content.onAppear {
analytics.track(.screenViewed(name: screenName))
}
}
}
extension View {
func trackScreen(_ name: String) -> some View {
modifier(AnalyticsScreenModifier(screenName: name))
}
}
// Usage
SettingsView()
.trackScreen("Settings")Privacy Considerations
User Consent
final class ConsentAwareAnalytics: AnalyticsService {
private let wrapped: AnalyticsService
private let consentProvider: () -> Bool
init(wrapped: AnalyticsService, consentProvider: @escaping () -> Bool) {
self.wrapped = wrapped
self.consentProvider = consentProvider
}
func track(_ event: AnalyticsEvent, properties: [String: String]) {
guard consentProvider() else { return }
wrapped.track(event, properties: properties)
}
}
// Usage
let analytics = ConsentAwareAnalytics(
wrapped: FirebaseAnalytics(),
consentProvider: { UserDefaults.standard.bool(forKey: "analyticsConsent") }
)What NOT to Track
- Personal identifiable information (PII)
- Exact timestamps (use day/hour granularity)
- Precise location (use region/country)
- Content of user-generated text
- Financial details
What's Safe to Track
- Feature usage (which features are used)
- App version and OS version
- Error types (not full stack traces)
- Session duration (rounded)
- Screen flow (which screens visited)
Testing
Unit Tests
class FeatureViewModelTests: XCTestCase {
func testButtonTracksEvent() {
let mockAnalytics = MockAnalytics()
let viewModel = FeatureViewModel(analytics: mockAnalytics)
viewModel.didTapButton()
XCTAssertEqual(mockAnalytics.trackedEvents.count, 1)
XCTAssertEqual(mockAnalytics.trackedEvents.first?.name, "button_tapped")
}
}
class MockAnalytics: AnalyticsService {
var trackedEvents: [AnalyticsEvent] = []
func track(_ event: AnalyticsEvent, properties: [String: String]) {
trackedEvents.append(event)
}
}SwiftUI Previews
#Preview {
ContentView()
.environment(\.analytics, NoOpAnalytics())
}Migration Strategy
Adding Analytics to Existing App
1. Add protocol and NoOp implementation 2. Wire up Environment 3. Add tracking calls with NoOp (verify builds) 4. Add real provider 5. Switch from NoOp to real provider
Switching Providers
1. Add new provider implementation 2. Test with subset of users (feature flag) 3. Monitor for issues 4. Switch all users 5. Remove old provider (optional)
import Foundation
/// App analytics events.
///
/// Add your app-specific events here. Use past tense for completed actions.
///
/// Example usage:
/// ```swift
/// analytics.track(.screenViewed(name: "Settings"))
/// analytics.track(.buttonTapped("subscribe"))
/// analytics.track(.featureUsed("dark_mode"))
/// ```
enum AnalyticsEvent: Sendable {
// MARK: - App Lifecycle
/// App launched (cold start)
case appLaunched
/// App moved to background
case appBackgrounded
/// App returned to foreground
case appForegrounded
// MARK: - Navigation
/// Screen was viewed
case screenViewed(name: String)
// MARK: - User Actions
/// Button was tapped
case buttonTapped(name: String)
/// Feature was used/enabled
case featureUsed(name: String)
/// Search performed
case searchPerformed(query: String)
// MARK: - Errors
/// Error occurred
case errorOccurred(domain: String, code: Int)
// MARK: - Custom Events
/// Custom event with properties
case custom(name: String, properties: [String: String])
// MARK: - Add Your Events Below
// Example:
// case itemCreated(type: String)
// case itemDeleted(type: String)
// case subscriptionStarted(plan: String)
// case settingsChanged(setting: String, value: String)
}
// MARK: - Event Names
extension AnalyticsEvent {
/// The event name string used for tracking.
var name: String {
switch self {
case .appLaunched:
return "app_launched"
case .appBackgrounded:
return "app_backgrounded"
case .appForegrounded:
return "app_foregrounded"
case .screenViewed(let name):
return "screen_viewed_\(name.lowercased().replacingOccurrences(of: " ", with: "_"))"
case .buttonTapped(let name):
return "button_tapped_\(name.lowercased())"
case .featureUsed(let name):
return "feature_used_\(name.lowercased())"
case .searchPerformed:
return "search_performed"
case .errorOccurred(let domain, let code):
return "error_\(domain.lowercased())_\(code)"
case .custom(let name, _):
return name
}
}
/// Additional properties for the event.
var properties: [String: String] {
switch self {
case .screenViewed(let name):
return ["screen_name": name]
case .buttonTapped(let name):
return ["button_name": name]
case .featureUsed(let name):
return ["feature_name": name]
case .searchPerformed(let query):
// Don't track exact query for privacy - just that search was used
return ["query_length": String(query.count)]
case .errorOccurred(let domain, let code):
return ["error_domain": domain, "error_code": String(code)]
case .custom(_, let properties):
return properties
default:
return [:]
}
}
}
import Foundation
/// Protocol defining the analytics service interface.
///
/// This protocol allows swapping analytics providers without changing app code.
/// Use `NoOpAnalytics` for testing and previews.
///
/// Example:
/// ```swift
/// // In App.swift
/// let analytics: AnalyticsService = TelemetryDeckAnalytics(appID: "...")
///
/// // To swap providers, change ONE line:
/// let analytics: AnalyticsService = FirebaseAnalytics()
/// ```
protocol AnalyticsService: Sendable {
/// Configure the analytics service. Call once at app launch.
func configure()
/// Track an event.
func track(_ event: AnalyticsEvent)
/// Track an event with additional properties.
func track(_ event: AnalyticsEvent, properties: [String: String])
/// Set a user property that persists across sessions.
func setUserProperty(_ key: String, value: String)
/// Set the user ID for attribution. Pass nil to clear.
func setUserID(_ id: String?)
/// Reset all user data. Call on logout.
func reset()
}
// MARK: - Default Implementations
extension AnalyticsService {
func track(_ event: AnalyticsEvent) {
track(event, properties: [:])
}
func setUserID(_ id: String?) {
// Optional - not all providers support this
}
func reset() {
// Default no-op
}
}
// MARK: - Common User Property Keys
enum AnalyticsUserProperty {
static let appVersion = "app_version"
static let subscriptionStatus = "subscription_status"
static let theme = "theme"
// Add your custom properties here
}
import SwiftUI
/// SwiftUI Environment key for analytics service.
///
/// Usage:
/// ```swift
/// // In App.swift
/// ContentView()
/// .environment(\.analytics, TelemetryDeckAnalytics(appID: "..."))
///
/// // In any View
/// struct MyView: View {
/// @Environment(\.analytics) private var analytics
///
/// var body: some View {
/// Button("Action") {
/// analytics.track(.buttonTapped("action"))
/// }
/// }
/// }
/// ```
private struct AnalyticsServiceKey: EnvironmentKey {
static let defaultValue: AnalyticsService = NoOpAnalytics()
}
extension EnvironmentValues {
var analytics: AnalyticsService {
get { self[AnalyticsServiceKey.self] }
set { self[AnalyticsServiceKey.self] = newValue }
}
}
// MARK: - Screen Tracking Modifier
/// View modifier for automatic screen tracking.
///
/// Usage:
/// ```swift
/// SettingsView()
/// .trackScreen("Settings")
/// ```
struct AnalyticsScreenModifier: ViewModifier {
let screenName: String
@Environment(\.analytics) private var analytics
func body(content: Content) -> some View {
content.onAppear {
analytics.track(.screenViewed(name: screenName))
}
}
}
extension View {
/// Track when this screen appears.
func trackScreen(_ name: String) -> some View {
modifier(AnalyticsScreenModifier(screenName: name))
}
}
import Foundation
import FirebaseCore
import FirebaseAnalytics
/// Firebase Analytics implementation.
///
/// Firebase Analytics provides comprehensive analytics with Google's infrastructure.
/// Note: May require user consent in EU/GDPR jurisdictions.
///
/// Setup:
/// 1. Add to Package.swift:
/// .package(url: "https://github.com/firebase/firebase-ios-sdk", from: "10.0.0")
/// // Add FirebaseAnalytics product
///
/// 2. Download GoogleService-Info.plist from Firebase Console
/// and add to your app target
///
/// 3. Initialize in App.swift:
/// let analytics: AnalyticsService = FirebaseAnalytics()
///
/// Usage:
/// ```swift
/// analytics.track(.screenViewed(name: "Settings"))
/// analytics.track(.buttonTapped("subscribe"))
/// ```
final class FirebaseAnalyticsService: AnalyticsService, @unchecked Sendable {
init() {}
func configure() {
// FirebaseApp.configure() should be called once at app launch
if FirebaseApp.app() == nil {
FirebaseApp.configure()
}
}
func track(_ event: AnalyticsEvent, properties: [String: String]) {
var parameters: [String: Any] = event.properties
for (key, value) in properties {
parameters[key] = value
}
Analytics.logEvent(event.firebaseName, parameters: parameters.isEmpty ? nil : parameters)
}
func setUserProperty(_ key: String, value: String) {
Analytics.setUserProperty(value, forName: key)
}
func setUserID(_ id: String?) {
Analytics.setUserID(id)
}
func reset() {
Analytics.setUserID(nil)
Analytics.resetAnalyticsData()
}
}
// MARK: - Firebase Event Names
extension AnalyticsEvent {
/// Firebase-compatible event name.
/// Firebase has specific naming requirements and reserved event names.
var firebaseName: String {
switch self {
case .appLaunched:
return "app_open" // Firebase reserved event
case .screenViewed:
return AnalyticsEventScreenView // Firebase reserved event
case .buttonTapped:
return "button_tap"
case .featureUsed:
return "feature_use"
case .searchPerformed:
return AnalyticsEventSearch // Firebase reserved event
case .errorOccurred:
return "app_error"
case .custom(let name, _):
return name
default:
return name.replacingOccurrences(of: "-", with: "_")
}
}
}
// MARK: - Consent Management
extension FirebaseAnalyticsService {
/// Set analytics collection based on user consent.
/// Call this when user grants or revokes consent.
static func setAnalyticsCollectionEnabled(_ enabled: Bool) {
Analytics.setAnalyticsCollectionEnabled(enabled)
}
}
import Foundation
/// No-operation analytics implementation.
///
/// Use for:
/// - Unit tests
/// - SwiftUI previews
/// - Users who opted out of analytics
/// - Debug builds (optional)
///
/// Example:
/// ```swift
/// // In tests
/// let analytics: AnalyticsService = NoOpAnalytics()
///
/// // In previews
/// #Preview {
/// ContentView()
/// .environment(\.analytics, NoOpAnalytics())
/// }
///
/// // For user opt-out
/// let analytics: AnalyticsService = userOptedIn ? TelemetryDeckAnalytics(...) : NoOpAnalytics()
/// ```
final class NoOpAnalytics: AnalyticsService, @unchecked Sendable {
init() {}
func configure() {
// No-op
}
func track(_ event: AnalyticsEvent, properties: [String: String]) {
// No-op
#if DEBUG
// Uncomment to see what would be tracked:
// print("[Analytics NoOp] \(event.name) - \(properties)")
#endif
}
func setUserProperty(_ key: String, value: String) {
// No-op
}
func setUserID(_ id: String?) {
// No-op
}
func reset() {
// No-op
}
}
import Foundation
import TelemetryClient
/// TelemetryDeck analytics implementation.
///
/// TelemetryDeck is a privacy-friendly analytics service that doesn't require
/// user consent in most jurisdictions (no cookies, no personal data).
///
/// Setup:
/// 1. Add to Package.swift:
/// .package(url: "https://github.com/TelemetryDeck/SwiftClient", from: "1.0.0")
///
/// 2. Get your App ID from https://dashboard.telemetrydeck.com
///
/// 3. Initialize in App.swift:
/// let analytics: AnalyticsService = TelemetryDeckAnalytics(appID: "YOUR-APP-ID")
///
/// Usage:
/// ```swift
/// analytics.track(.screenViewed(name: "Settings"))
/// analytics.track(.buttonTapped("subscribe"))
/// ```
final class TelemetryDeckAnalytics: AnalyticsService, @unchecked Sendable {
private let appID: String
/// Initialize with your TelemetryDeck App ID.
/// - Parameter appID: Your app ID from TelemetryDeck dashboard
init(appID: String) {
self.appID = appID
}
func configure() {
let config = TelemetryManagerConfiguration(appID: appID)
TelemetryManager.initialize(with: config)
}
func track(_ event: AnalyticsEvent, properties: [String: String]) {
var allProperties = event.properties
for (key, value) in properties {
allProperties[key] = value
}
TelemetryManager.send(event.name, with: allProperties)
}
func setUserProperty(_ key: String, value: String) {
// TelemetryDeck uses default user properties from the SDK
// Custom properties are sent with each signal
// Store locally if you need to include in all future events
}
func setUserID(_ id: String?) {
// TelemetryDeck generates anonymous user IDs automatically
// No need to set explicitly
}
func reset() {
// TelemetryDeck doesn't store user state that needs resetting
}
}
// MARK: - Configuration Options
extension TelemetryDeckAnalytics {
/// Create with additional configuration options.
static func configured(
appID: String,
salt: String? = nil,
testMode: Bool = false
) -> TelemetryDeckAnalytics {
let analytics = TelemetryDeckAnalytics(appID: appID)
// Configure with options if needed
var config = TelemetryManagerConfiguration(appID: appID)
if let salt = salt {
config.salt = salt
}
config.testMode = testMode
TelemetryManager.initialize(with: config)
return analytics
}
}