# klime
Klime SDK for Elixir.
## Installation
Add `klime` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:klime, "~> 1.0"}
]
end
```
Then run:
```bash
mix deps.get
```
## Quick Start
```elixir
# Start the client (typically in your application supervision tree)
{:ok, client} = Klime.Client.start_link(write_key: "your-write-key")
# Identify a user
Klime.identify(client, "user_123", %{
email: "user@example.com",
name: "Stefan"
})
# Track an event
Klime.track(client, "Button Clicked", %{
button_name: "Sign up",
plan: "pro"
}, user_id: "user_123")
# Associate user with a group and set group traits
Klime.group(client, "org_456", %{
name: "Acme Inc",
plan: "enterprise"
}, user_id: "user_123")
# Or just link the user to a group (if traits are already set)
Klime.group(client, "org_456", %{}, user_id: "user_123")
# Shutdown gracefully
Klime.shutdown(client)
```
## Installation Prompt
Copy and paste this prompt into Cursor, Copilot, or your favorite AI editor to integrate Klime:
```
Integrate Klime for customer analytics. Klime tracks user activity to identify which customers are healthy vs at risk of churning.
ANALYTICS MODES (determine which applies):
- Companies & Teams: Your customers are companies with multiple team members (SaaS, enterprise tools)
→ Use identify() + group() + track()
- Individual Customers: Your customers are individuals with private accounts (consumer apps, creator tools)
→ Use identify() + track() only (no group() needed)
KEY CONCEPTS:
- Every track() call requires either user_id OR group_id (no anonymous events)
- Use group_id alone for org-level events (webhooks, cron jobs, system metrics)
- group() links a user to a company AND sets company traits (only for Companies & Teams mode)
- Order doesn't matter - events before identify/group still get attributed correctly
BEST PRACTICES:
- Add Klime.Client to your application supervision tree
- Store write key in KLIME_WRITE_KEY environment variable
- Client automatically handles graceful shutdown when supervisor stops
Add to mix.exs: {:klime, "~> 1.0"}
Then run: mix deps.get
# In application.ex
children = [
{Klime.Client, write_key: System.get_env("KLIME_WRITE_KEY"), name: Klime}
]
# Identify users at signup/login:
Klime.identify(Klime, "usr_abc123", %{email: "jane@acme.com", name: "Jane Smith"})
# Track key activities:
Klime.track(Klime, "Report Generated", %{report_type: "revenue"}, user_id: "usr_abc123")
Klime.track(Klime, "Feature Used", %{feature: "export", format: "csv"}, user_id: "usr_abc123")
Klime.track(Klime, "Teammate Invited", %{role: "member"}, user_id: "usr_abc123")
# If Companies & Teams mode: link user to their company and set company traits
Klime.group(Klime, "org_456", %{name: "Acme Inc", plan: "enterprise"}, user_id: "usr_abc123")
INTEGRATION WORKFLOW:
Phase 1: Discover
Explore the codebase to understand:
1. What framework is used? (Phoenix, plug, Bandit, etc.)
2. Where is user identity available? (e.g., conn.assigns.current_user, socket.assigns.current_user)
3. Is this Companies & Teams or Individual Customers?
- Look for: organization, workspace, tenant, team, account schemas → Companies & Teams (use group())
- No company/org concept, just individual users → Individual Customers (skip group())
4. Where do core user actions happen? (controllers, live views, channels, contexts)
5. Is there existing analytics? (search: segment, posthog, mixpanel, amplitude, track)
Match your integration style to the framework's conventions.
Phase 2: Instrument
Add these calls using idiomatic patterns for the framework:
- Add Klime.Client to application.ex supervision tree
- identify() in auth/login success handler
- group() when user-org association is established (Companies & Teams mode only)
- track() for key user actions (see below)
WHAT TO TRACK:
Active engagement (primary): feature usage, resource creation, collaboration, completing flows
Session signals (secondary): login/session start, dashboard access - distinguishes "low usage" from "churned"
Do NOT track: every request, health checks, plugs that run on every request, background jobs
Phase 3: Verify
Confirm: client in supervision tree, identify/group/track calls added
Phase 4: Summarize
Report what you added:
- Files modified and what was added to each
- Events being tracked (list event names and what triggers them)
- How user_id is obtained (and group_id if Companies & Teams mode)
- Any assumptions made or questions
```
## API Reference
### Starting the Client
```elixir
# Option 1: Start directly
{:ok, client} = Klime.Client.start_link(
write_key: "your-write-key", # Required
endpoint: "https://i.klime.com", # Optional (default)
flush_interval: 2000, # Optional: ms between flushes (default: 2000)
max_batch_size: 20, # Optional: max events per batch (default: 20, max: 100)
max_queue_size: 1000, # Optional: max queued events (default: 1000)
retry_max_attempts: 5, # Optional: max retry attempts (default: 5)
retry_initial_delay: 1000, # Optional: initial retry delay in ms (default: 1000)
flush_on_shutdown: true, # Optional: auto-flush on shutdown (default: true)
on_error: &handle_error/2, # Optional: callback for batch failures
on_success: &handle_success/1, # Optional: callback for successful sends
name: MyApp.Klime # Optional: registered name
)
# Option 2: Add to supervision tree (recommended for Phoenix apps)
children = [
{Klime.Client, write_key: System.get_env("KLIME_WRITE_KEY"), name: Klime}
]
```
### Methods
#### `track(client, event_name, properties \\ %{}, opts \\ [])`
Track an event. Events can be attributed in two ways:
- **User events**: Provide `user_id:` to track user activity (most common)
- **Group events**: Provide `group_id:` without `user_id:` for organization-level events
```elixir
# User event (most common)
Klime.track(client, "Button Clicked", %{
button_name: "Sign up",
plan: "pro"
}, user_id: "user_123")
# Group event (for webhooks, cron jobs, system events)
Klime.track(client, "Events Received", %{
count: 100,
source: "webhook"
}, group_id: "org_456")
```
> **Note**: The `group_id:` option can also be combined with `user_id:` for multi-tenant scenarios where you need to specify which organization context a user event occurred in.
### Synchronous Methods (Bang Methods)
For cases where you need guaranteed delivery or want to handle errors explicitly, use the synchronous versions that block until the event is sent:
```elixir
# Sync track - blocks until sent, returns {:ok, response} or {:error, error}
{:ok, response} = Klime.track!(client, "Button Clicked", %{button: "signup"}, user_id: "user_123")
# Sync identify
{:ok, response} = Klime.identify!(client, "user_123", %{email: "user@example.com"})
# Sync group
{:ok, response} = Klime.group!(client, "org_456", %{name: "Acme Inc"}, user_id: "user_123")
```
These methods:
- Send the event immediately (no batching)
- Block until the HTTP request completes
- Return `{:ok, %Klime.BatchResponse{}}` on success
- Return `{:error, %Klime.SendError{}}` on failure
Use sync methods sparingly - they add latency to your code. The async methods are preferred for most use cases.
#### `identify(client, user_id, traits \\ %{})`
Identify a user with traits.
```elixir
Klime.identify(client, "user_123", %{
email: "user@example.com",
name: "Stefan"
})
```
#### `group(client, group_id, traits \\ %{}, opts \\ [])`
Associate a user with a group and/or set group traits.
```elixir
# Associate user with a group and set group traits (most common)
Klime.group(client, "org_456", %{
name: "Acme Inc",
plan: "enterprise"
}, user_id: "user_123")
# Just link a user to a group (traits already set or not needed)
Klime.group(client, "org_456", %{}, user_id: "user_123")
# Just update group traits (e.g., from a webhook or background job)
Klime.group(client, "org_456", %{
plan: "enterprise",
employee_count: 50
})
```
#### `flush(client)`
Manually flush queued events immediately.
```elixir
:ok = Klime.flush(client)
```
#### `shutdown(client)`
Gracefully shutdown the client, flushing remaining events.
```elixir
:ok = Klime.shutdown(client)
```
## Features
- **Automatic Batching**: Events are automatically batched and sent every 2 seconds or when the batch size reaches 20 events
- **Automatic Retries**: Failed requests are automatically retried with exponential backoff
- **Async & Sync Methods**: Use async methods for fire-and-forget, or sync (`track!`, `identify!`, `group!`) for guaranteed delivery
- **OTP Supervision**: GenServer-based client integrates naturally with OTP supervision trees
- **Plug Middleware**: Optional `Klime.Plug` for per-request flush in Phoenix/Plug apps
- **Graceful Shutdown**: Automatically flushes events when the supervisor stops (with `flush_on_shutdown: true`)
- **Callbacks**: `on_error` and `on_success` callbacks for monitoring
- **Minimal Dependencies**: Only requires `jason` for JSON encoding (`plug` optional for middleware)
## Performance
When you call `track/4`, `identify/3`, or `group/4`, the SDK:
1. Adds the event to an in-memory queue (microseconds)
2. Returns immediately without waiting for network I/O
Events are sent to Klime's servers asynchronously. This means:
- **No network blocking**: HTTP requests happen in the GenServer process, not your request handler
- **No latency impact**: Tracking calls add < 1ms to your request handling time
- **Automatic batching**: Events are queued and sent in batches (default: every 2 seconds or 20 events)
```elixir
# This returns immediately - no HTTP request is made here
Klime.track(client, "Button Clicked", %{button: "signup"}, user_id: "user_123")
# Your code continues without waiting
json(conn, %{success: true})
```
The only blocking operation is `flush/1`, which waits for all queued events to be sent. This is typically only called during graceful shutdown.
## Configuration
### Default Values
- `flush_interval`: 2000ms
- `max_batch_size`: 20 events
- `max_queue_size`: 1000 events
- `retry_max_attempts`: 5 attempts
- `retry_initial_delay`: 1000ms
- `flush_on_shutdown`: true
### Callbacks
```elixir
{Klime.Client,
write_key: System.get_env("KLIME_WRITE_KEY"),
name: Klime,
on_error: fn error, _events ->
Logger.error("Klime error: #{inspect(error)}")
Sentry.capture_exception(error)
end,
on_success: fn response ->
Logger.info("Sent #{response.accepted} events")
end
}
```
### Plug Middleware
For guaranteed per-request delivery, use `Klime.Plug` to flush events after each request:
```elixir
# In your Phoenix endpoint.ex or router.ex
plug Klime.Plug, client: Klime
```
> **Note**: This adds latency to every request as it waits for the flush.
> Only use this if you need guaranteed per-request delivery.
> For most use cases, the background worker is sufficient.
## Error Handling
The SDK automatically handles:
- **Transient errors** (429, 503, network failures): Retries with exponential backoff
- **Permanent errors** (400, 401): Logs error and drops event
- **Rate limiting**: Respects `Retry-After` header
## Size Limits
- Maximum event size: 200KB
- Maximum batch size: 10MB
- Maximum events per batch: 100
Events exceeding these limits are rejected and logged.
## Phoenix Example
```elixir
# lib/my_app/application.ex
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
MyAppWeb.Endpoint,
{Klime.Client, write_key: System.get_env("KLIME_WRITE_KEY"), name: Klime}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
```
```elixir
# lib/my_app_web/controllers/button_controller.ex
defmodule MyAppWeb.ButtonController do
use MyAppWeb, :controller
def click(conn, %{"button_name" => button_name}) do
user_id = conn.assigns[:current_user] && conn.assigns.current_user.id
Klime.track(Klime, "Button Clicked", %{
button_name: button_name
}, user_id: user_id)
json(conn, %{success: true})
end
end
```
## Phoenix LiveView Example
```elixir
# lib/my_app_web/live/dashboard_live.ex
defmodule MyAppWeb.DashboardLive do
use MyAppWeb, :live_view
def mount(_params, _session, socket) do
user = socket.assigns.current_user
Klime.track(Klime, "Dashboard Viewed", %{}, user_id: user.id)
{:ok, socket}
end
def handle_event("export", %{"format" => format}, socket) do
user = socket.assigns.current_user
Klime.track(Klime, "Export Clicked", %{format: format}, user_id: user.id)
{:noreply, socket}
end
end
```
## Requirements
- Elixir 1.15 or higher
- OTP 25 or higher
- `jason` ~> 1.4
## License
MIT