Skip to content

Counting and querying events recorded over time

License

Notifications You must be signed in to change notification settings

jhugman/tiny-counter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

tiny-counter

CI

Record app usage to drive on-device decisions in mobile and desktop apps.

Count events as they happen. Query aggregates later when you know what questions matter. Details fade automatically—only frequency and recency remain.

Answer questions about events per time:

  • How many events in the previous N time units?
  • How many time units had one or more events?
  • When did an event last occur, or first occur?

For example, recording just app launches answers:

  • Has the user used the app in 15 of the last 28 days?
  • Has the app launched today? Was it yesterday?
  • How often does the user visit settings?
  • When did the user last launch the app?
  • Has usage increased this week vs last week?

Drive app behavior:

  • Show tooltip? (user saved 3 passwords this week → suggest Sync setup)
  • Run recovery migration? (user saw error recently)
  • Rate limit API? (spread usage across sessions for good citizenship)

Event data:

  • Stays on device for privacy
  • No exact timestamps stored—only counts per time period
  • Details automatically fade as buckets rotate
  • Uses 1KB per event (100 events = 100KB)
  • Thread safe

Product decisions care about frequency and recency, not individual actions. Aggregate patterns matter; specific details fade over time.

Core Example

use tiny_counter::EventStore;

let store = EventStore::new();

store.record("app_launch");
store.record("app_launch");

let launches = store.query("app_launch").last_days(7).sum().unwrap_or(0);
assert_eq!(launches, 2);

When To Use It

Good for:

  • On-device usage analytics
  • Feature adoption metrics
  • Rate limiting APIs and user actions
  • System monitoring (error rates, throughput)

Not for:

  • Audit logs (events fall off after tracking window)
  • Exact timestamps (only counts per time bucket)
  • Unbounded history (fixed-size storage)

Installation

[dependencies]
tiny-counter = "0.1"

# With optional features
tiny-counter = { version = "0.1", features = ["storage-sqlite", "serde-json", "tokio"] }

# For uniform 30-day months instead of calendar months
tiny-counter = { version = "0.1", default-features = false, features = ["storage-fs", "serde-bincode"] }

How It Works

Rotating buckets: Events drop into time buckets. Bucket 0 is "today" (since midnight) or "this hour" (since :00). Bucket 1 is "yesterday" or "last hour." Time advances → buckets rotate → old data falls off.

Multiple time units: One record() updates all time units (minutes, hours, days, months). Query any scale without reprocessing.

Fixed memory: Each event type uses ~1KB (default: 256 buckets × 4 bytes). 200 events = 200KB.

Tradeoff: Trade precision for memory. You get "10 events in last hour" but not exact timestamps. Events older than the tracking window drop silently.

Configuration

Default config tracks 256 total buckets:

  • 60 Minutes (last hour)
  • 72 Hours (last 3 days)
  • 56 Days (last ~8 weeks)
  • 52 Weeks (last year)
  • 12 Months (last year)
  • 4 Years

Customize with builder:

use tiny_counter::EventStore;

let store = EventStore::builder()
    .track_hours(24)
    .track_days(28)
    .track_weeks(26)
    .track_years(2)
    .build()
    .unwrap();

Configuration changes are handled automatically—change bucket counts anytime and existing data adapts on load.

Quick Examples

Recording and Querying

// Record events
store.record("app_launch");
store.record_count("api_call", 5);

// Query time windows
let last_hour = store.query("api_call").last_hours(1).sum().unwrap_or(0);
let last_week = store.query("app_launch").last_days(7).sum().unwrap_or(0);

// Count active periods
let active_days = store.query("app_launch").last_days(28).count_nonzero().unwrap_or(0);

// When did event last occur?
if let Some(duration) = store.query("error:sync").last_seen() {
    println!("Last error {} minutes ago", duration.num_minutes());
}

Persistence

use tiny_counter::{EventStore, storage::Sqlite};

let store = EventStore::builder()
    .with_storage(Sqlite::open("events.db")?)
    .build()?;

store.record("page_view");
store.persist()?;  // Save to disk

Rate Limiting

use tiny_counter::TimeUnit;

// Enforce multiple limits
match store.limit()
    .at_most("api_call", 10, TimeUnit::Minutes)
    .at_most("api_call", 100, TimeUnit::Hours)
    .check_and_record("api_call")
{
    Ok(_) => println!("Request allowed"),
    Err(e) => println!("Rate limited: {}", e),
}

Transactional Reservations

Prevent race conditions with atomic check-and-reserve:

let reservation = store.limit()
    .at_most("payment", 1, TimeUnit::Minutes)
    .reserve("payment")?;

match process_payment() {
    Ok(_) => reservation.commit(),  // Count it
    Err(_) => reservation.cancel(), // Release slot
}

Comparing Events

// Conversion rate
let conversion = store.query_ratio("purchases", "visits").last_days(7);

// Net change (inventory, balance, connections)
let balance = store.query_delta("deposits", "withdrawals").ever().sum();

Multi-Device Sync

// Export dirty counters from device 1
let device1_data = device1_store.export_dirty()?;

// Merge into device 2
device2_store.merge_all(device1_data)?;

Features

Storage backends:

  • storage-fs - File-per-event (default)
  • storage-sqlite - SQLite database (all events in one DB)
  • MemoryStorage - No persistence (testing)

Serialization formats:

  • serde-bincode - Compact binary (default)
  • serde-json - Human-readable JSON

Time bucket behavior:

  • calendar (default) - Days/weeks/months rotate at local midnight
  • Disable with default-features = false for uniform 30-day months and 24-hour days

Optional features:

  • tokio - Auto-persist with background task

Mix and match any storage with any format.

Rate Limiting

Constraint types:

  • at_most - Maximum N per window (typical rate limit)
  • at_least - Minimum N required (prerequisite check)
  • cooldown - Minimum time between events
  • within - Event must have occurred recently
  • during/outside_of - Schedule-based (business hours, weekdays)

Combine constraints:

use std::time::Duration;
use tiny_counter::Schedule;

store.limit()
    .at_most("api", 100, TimeUnit::Hours)
    .at_least("login", 1, TimeUnit::Days)        // Must be logged in
    .cooldown("reset", Duration::minutes(5))     // Wait between resets
    .during(Schedule::hours(9, 17))              // Business hours only
    .check_and_record("api")?;

Documentation

  • REFERENCE.md - Complete API guide with progressive disclosure
  • COOKBOOK.md - Advanced patterns combining features
  • DESIGN.md - Architecture for contributors
  • Examples - Runnable code:
    • Core: basic, type_safety, persistence
    • Use cases: rate_limiting, analytics, multi_device, resource_tracking, security, concurrent
    • Features: async_autopersist (tokio)

License

MIT OR Apache-2.0

About

Counting and querying events recorded over time

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages