README.md

# 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