Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Generates user-facing usage statistics, activity summaries, and personalized insights dashboards (weekly recaps, year-in-review, Spotify Wrapped-style). Use when user wants to show usage stats, activity insights, or shareable recap screens. Different from analytics-setup which sends data to a backend — this shows insights to the USER on-device.
.claude/skills/rshankras-usage-insights/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | -32% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 110% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 125% | 0% |
| case-16 | ✗→✓ | ▲ Improved | 131% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 127% | 0% |
Generate a production usage insights system that records user activity events with SwiftData, computes personalized insights (streaks, most active day, top categories), and displays them in a dashboard with insight cards, period pickers, trend indicators, and optional shareable recap screens.
Use this skill when the user:
Search for existing usage tracking or insights code:
Glob: **/*UsageEvent*.swift, **/*Insight*.swift, **/*Recap*.swift, **/*ActivityLog*.swift
Grep: "UsageEvent" or "InsightCalculator" or "activitySummary" or "SwiftData" and "event"If existing analytics/tracking found:
InsightCalculator to work with existing modelsCheck for SwiftData usage:
Grep: "import SwiftData" or "@Model" or "ModelContainer"If SwiftData already in use:
UsageEvent into existing ModelContainerIf no SwiftData:
ModelContainer configurationAsk user via AskUserQuestion:
UsageEvent model and recorder) — recommendedRead templates.md for production Swift code.
Generate these files:
UsageEvent.swift — SwiftData @Model for recording user activity eventsInsightResult.swift — Model for computed insights (title, value, trend, visualization type)InsightCalculator.swift — Pure functions that aggregate events into insightsInsightsDashboardView.swift — Main dashboard with grid of insight cards and period pickerInsightCardView.swift — Individual insight card with icon, value, trend indicator, sparklineBased on configuration:
UsageRecapView.swift — If shareable recap selected (paged summary with share card generation)UsageEventRecorder.swift — If SwiftData data source selected (convenience class for recording events)Check project structure:
Sources/ exists -> Sources/UsageInsights/App/ exists -> App/UsageInsights/UsageInsights/After generation, provide:
UsageInsights/
├── UsageEvent.swift # SwiftData @Model for activity events
├── InsightResult.swift # Computed insight model
├── InsightCalculator.swift # Aggregation engine
├── InsightsDashboardView.swift # Dashboard with period picker
├── InsightCardView.swift # Individual insight card
├── UsageRecapView.swift # Shareable recap (optional)
└── UsageEventRecorder.swift # Event recording helper (optional)Add ModelContainer (if not already present):
swift@main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: [UsageEvent.self]) } }
Record events from anywhere in the app:
swiftstruct TaskDetailView: View { @Environment(\.modelContext) private var modelContext @State private var recorder: UsageEventRecorder? var body: some View { Button("Complete Task") { completeTask() recorder?.record( .taskCompleted, metadata: ["category": "work", "priority": "high"] ) } .onAppear { recorder = UsageEventRecorder(modelContext: modelContext) } } }
Show the insights dashboard:
swiftNavigationLink("My Insights") { InsightsDashboardView() }
Show a weekly recap:
swiftstruct WeeklyRecapSheet: View { @Environment(\.modelContext) private var modelContext @State private var showRecap = false var body: some View { Button("View Weekly Recap") { showRecap = true } .sheet(isPresented: $showRecap) { UsageRecapView(period: .week) } } }
Record events with duration tracking:
swift// Start a timed session let sessionStart = Date() // ... user does work ... // Record when session ends recorder?.record( .sessionCompleted, metadata: ["screen": "editor"], duration: Date().timeIntervalSince(sessionStart) )
swift@Test func calculatesWeeklySummaryCorrectly() async throws { let calendar = Calendar.current let now = Date() let events: [UsageEvent] = (0..<7).flatMap { dayOffset in let date = calendar.date(byAdding: .day, value: -dayOffset, to: now)! return (0..<(dayOffset == 2 ? 5 : 2)).map { _ in UsageEvent(eventType: "taskCompleted", timestamp: date) } } let calculator = InsightCalculator() let insights = calculator.weeklySummary(from: events, referenceDate: now) let totalInsight = insights.first { $0.title == "Total Events" } #expect(totalInsight != nil) #expect(totalInsight?.value == "19") // 5 + (6 * 2) } @Test func identifiesMostActiveDay() async throws { let calendar = Calendar.current let now = Date() // Create 5 events on Wednesday, 2 on other days var events: [UsageEvent] = [] for dayOffset in 0..<7 { let date = calendar.date(byAdding: .day, value: -dayOffset, to: now)! let count = calendar.component(.weekday, from: date) == 4 ? 5 : 1 for _ in 0..<count { events.append(UsageEvent(eventType: "action", timestamp: date)) } } let calculator = InsightCalculator() let insights = calculator.weeklySummary(from: events, referenceDate: now) let mostActive = insights.first { $0.title == "Most Active Day" } #expect(mostActive?.value == "Wednesday") } @Test func handlesEmptyEventList() async throws { let calculator = InsightCalculator() let insights = calculator.weeklySummary(from: [], referenceDate: Date()) #expect(!insights.isEmpty) // Should still return cards with zero values let totalInsight = insights.first { $0.title == "Total Events" } #expect(totalInsight?.value == "0") } @Test func recorderBatchesWritesForPerformance() async throws { let config = ModelConfiguration(isStoredInMemoryOnly: true) let container = try ModelContainer(for: UsageEvent.self, configurations: config) let context = ModelContext(container) let recorder = UsageEventRecorder(modelContext: context) // Record 100 events rapidly for i in 0..<100 { recorder.record(.custom("event_\(i)")) } // Flush pending writes recorder.flush() let descriptor = FetchDescriptor<UsageEvent>() let count = try context.fetchCount(descriptor) #expect(count == 100) }
swiftrecorder?.record(.featureUsed, metadata: ["feature": "darkMode"])
swiftlet calculator = InsightCalculator() let events = try modelContext.fetch(FetchDescriptor<UsageEvent>()) let insights = calculator.weeklySummary(from: events, referenceDate: Date())
swiftLazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 16) { ForEach(insights) { insight in InsightCardView(insight: insight) } }
swiftlet renderer = ImageRenderer(content: UsageRecapView(period: .week)) renderer.scale = 2.0 if let image = renderer.uiImage { // Use with ShareLink or UIActivityViewController }
analytics-setup is for)#Predicate with date range)UsageEventRecorder)Calendar.current for date math — never assume 7 days = 1 weekcalendar.firstWeekday)calendar.dateInterval(of: .weekOfYear, for: date) for accurate week boundaries.chartYScale(domain:) to prevent axis from starting at a misleading value.accessibilityLabel on chart marksgenerators/share-card — Render recap views as shareable imagesgenerators/analytics-setup — Backend analytics (complements on-device insights)generators/streak-tracker — Streak tracking pairs well with usage insightsgenerators/milestone-celebration — Celebrate milestones surfaced by insightsOther measured skills in the registry, with their headline benchmark lift.