Stint (stint v0.1.0)

Copy Markdown View Source

Tick-based activity session tracking. No start/stop calls — report elapsed time as it happens, and stints (bounded periods of activity) assemble themselves.

# on every activity ping (a reader's progress tick, a player's
# watch-time beat, a practice timer):
Stint.track(user_id, "manga:mangadex/one-piece", 30)

If the owner's latest stint on that item ended within the gap window (default 5 minutes), the tick extends it — ended_at moves to now, seconds accumulates. A longer silence means the next tick opens a new stint. That inference is the whole point: session ends are unobservable in real apps (tabs close, phones lock, processes die), while periodic ticks are easy and reliable. A "session" stops being an API anyone can forget to call and becomes an emergent fact of the data.

Each stint carries second-resolution started_at / ended_at, so the data distinguishes one two-hour binge from six three-minute peeks — not just "N minutes in hour H".

Setup

config :stint, repo: MyApp.Repo

And a migration:

def up, do: Stint.Migration.up()
def down, do: Stint.Migration.down()

Identity

owner_id and item are opaque strings — a user UUID, a device id, "manga:source/slug", "habit:guitar" — the library never interprets them.

Options

  • :gap — seconds of silence that split stints (default 300, configurable app-wide via config :stint, default_gap: n)
  • :at — the tick's timestamp (default DateTime.utc_now/0)
  • :meta — map shallow-merged into the stint's meta on every tick (last write wins per key)

Timezone-aware day queries take :utc_offset in seconds (e.g. a UTC+2 user is utc_offset: 7200).

Summary

Functions

Clamp a stint's interval to the owner's local date — the slice a day view should render for a midnight-crossing stint. Returns {started_at, ended_at} in UTC, or nil when the stint doesn't touch the date.

Number of stints by owner on item — "read in 14 stints".

Stint counts per item for an owner: %{item => count}.

The owner's most recent stint, optionally per item.

Stints intersecting the owner's local date, oldest first. A stint crossing midnight appears on both dates it touches — clamp for display with clamp_to_date/3 if needed.

Total tracked seconds for an owner, optionally per item.

Record seconds of activity by owner on item.

Types

item()

@type item() :: String.t()

owner()

@type owner() :: String.t()

Functions

clamp_to_date(stint, date, opts \\ [])

@spec clamp_to_date(Stint.Record.t(), Date.t(), keyword()) ::
  {DateTime.t(), DateTime.t()} | nil

Clamp a stint's interval to the owner's local date — the slice a day view should render for a midnight-crossing stint. Returns {started_at, ended_at} in UTC, or nil when the stint doesn't touch the date.

count(owner, item)

@spec count(owner(), item()) :: non_neg_integer()

Number of stints by owner on item — "read in 14 stints".

counts(owner)

@spec counts(owner()) :: %{required(item()) => non_neg_integer()}

Stint counts per item for an owner: %{item => count}.

last(owner, item \\ nil)

@spec last(owner(), item() | nil) :: Stint.Record.t() | nil

The owner's most recent stint, optionally per item.

on_date(owner, date, opts \\ [])

@spec on_date(owner(), Date.t(), keyword()) :: [Stint.Record.t()]

Stints intersecting the owner's local date, oldest first. A stint crossing midnight appears on both dates it touches — clamp for display with clamp_to_date/3 if needed.

Options: :utc_offset (seconds, default 0), :item to filter.

total_seconds(owner, item \\ nil)

@spec total_seconds(owner(), item() | nil) :: non_neg_integer()

Total tracked seconds for an owner, optionally per item.

track(owner, item, seconds, opts \\ [])

@spec track(owner(), item(), non_neg_integer(), keyword()) ::
  {:ok, Stint.Record.t(), :extended | :started} | {:error, term()}

Record seconds of activity by owner on item.

Extends the latest stint when its end is within the gap window of :at; opens a new stint otherwise (with started_at back-dated by seconds, so the first tick doesn't lose its own duration).

Returns {:ok, stint, :extended | :started}.