# Audiences Source: https://docs.tell.rs/analytics/audiences Define reusable user groups based on properties and behavior, then apply them to any metric. Audiences are saved user groups defined by rules. Combine property filters ("country is US") with behavioral rules ("did purchase in last 30 days") to create a group, then use it to scope any metric — DAU, funnels, retention, events, and more. ## Create an audience ```bash theme={null} curl -X POST https://your-tell-server/api/v1/audiences \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{ "name": "US power buyers", "description": "US users who purchased in the last 30 days", "definition": { "filters": [ { "field": "country", "operator": "eq", "value": "US" } ], "events": [ { "event": "purchase", "days": 30 } ] } }' ``` Response: ```json theme={null} { "id": "aud_x7k9m2", "workspace_id": "ws_abc123", "owner_id": "usr_abc", "name": "US power buyers", "description": "US users who purchased in the last 30 days", "definition": { "filters": [ { "field": "country", "operator": "eq", "value": "US" } ], "events": [ { "event": "purchase", "days": 30, "filters": [] } ] }, "created_at": "2025-06-15T10:30:00Z", "updated_at": "2025-06-15T10:30:00Z" } ``` Tell resolves the audience rules into a set of matching device IDs and injects them as a filter into any metric query. All rules are AND-combined — users must match every filter and every behavioral rule. ## Definition syntax An audience definition has three parts: property filters, behavioral event rules, and an optional lookback window. ### Property filters Each filter matches a field against a value using an [operator](/analytics/filtering#operators): ```json theme={null} { "filters": [ { "field": "country", "operator": "eq", "value": "US" }, { "field": "device_type", "operator": "in", "value": ["mobile", "tablet"] } ] } ``` **Field prefixes:** | Prefix | Source | Example | | - | - | - | | *(none)* | Event top-level fields | `country`, `device_type`, `operating_system` | | `properties.` | Custom event properties | `properties.plan`, `properties.amount` | | `trait:` | User traits (set via `identify`) | `trait:plan`, `trait:company` | Trait filters join through the `user_traits` and `user_devices` tables automatically. ### Behavioral rules Each rule matches users who performed a specific event within a time window: ```json theme={null} { "events": [ { "event": "purchase", "days": 30, "filters": [ { "field": "properties.amount", "operator": "gte", "value": "100" } ] } ] } ``` This matches users who made a purchase of \$100 or more in the last 30 days. The `filters` array on each event rule is optional — omit it to match any occurrence of the event. ### Lookback window Set `lookback_days` to limit how far back Tell scans when resolving property filters. This improves query performance on large datasets: ```json theme={null} { "filters": [ { "field": "country", "operator": "eq", "value": "US" } ], "events": [ { "event": "purchase", "days": 30 } ], "lookback_days": 90 } ``` Behavioral rules always use their own `days` window. The `lookback_days` field only affects property filters. ## Check member count See how many users match an audience: ```bash theme={null} curl https://your-tell-server/api/v1/audiences/aud_x7k9m2/count \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` ```json theme={null} { "audience_id": "aud_x7k9m2", "count": 3842 } ``` ## Use audiences in metrics Audiences filter every metric type. Pass the audience definition as part of the [filter system](/analytics/filtering) to scope any query — DAU, events, [funnels](/analytics/funnels), retention, lifecycle, and more. This is handled at the query engine level: Tell resolves the audience to a set of device IDs, then restricts all metric queries to those devices. ## Manage audiences ### List audiences ```bash theme={null} curl https://your-tell-server/api/v1/audiences \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` Returns the audiences in the workspace as a [list response](/api/authentication#list-responses): one page in `items` (default 50, max 200) and the full count in `total`. ### Get an audience ```bash theme={null} curl https://your-tell-server/api/v1/audiences/aud_x7k9m2 \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` ### Update an audience ```bash theme={null} curl -X PUT https://your-tell-server/api/v1/audiences/aud_x7k9m2 \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{ "name": "US power buyers (updated)", "definition": { "filters": [ { "field": "country", "operator": "in", "value": ["US", "CA"] } ], "events": [ { "event": "purchase", "days": 30 } ] } }' ``` Only the audience owner or a workspace admin can update an audience. All fields are optional — omit a field to keep its current value. ### Delete an audience ```bash theme={null} curl -X DELETE https://your-tell-server/api/v1/audiences/aud_x7k9m2 \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` Only the audience owner or a workspace admin can delete an audience. ## Audiences vs segments [Segments](/analytics/segments) are lifecycle-based groups (new, returning, dormant, churned) computed automatically by Tell. Audiences are rule-based groups you define explicitly. Use segments for lifecycle analysis. Use audiences to target specific user populations across any metric. ## What's next * [Filtering](/analytics/filtering) — the filter system that audiences plug into * [Funnels](/analytics/funnels) — measure conversion for a specific audience * [Segments](/analytics/segments) — lifecycle-based user groups * [Data queries API](/api/queries) — full API reference for audience endpoints # Boards & Dashboards Source: https://docs.tell.rs/analytics/boards Build dashboards with metric blocks, notes, and grid layout to visualize your analytics. Boards are your team's dashboards. Each board holds metric blocks that visualize your data — active users, event counts, sessions, log volume — alongside markdown notes for context and commentary. ## Creating a board Create a board from the API, the TUI, or through an AI assistant via [MCP](/tools/mcp): ```bash theme={null} curl -X POST https://your-tell-server/api/v1/boards \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{"title": "Product Overview"}' ``` Give it a descriptive title. Your team will scan board titles when deciding where to look. ## Metric blocks Each board is made up of blocks. A **metric block** displays a chart, number, or table powered by a Tell query. You can have up to 50 metric blocks per board, each with up to 20 data series. Typical blocks: * **DAU/WAU/MAU** — active user trends over 30 or 90 days * **Top events** — most frequent events ranked by count * **Conversion metrics** — custom aggregations like sum of `purchase.amount` * **Log volume** — error rate or log count by level over time * **Treemap** — [grouped metrics](/analytics/events#grouped-metrics) showing proportional composition with size and intensity * **Funnel** — step-by-step [conversion analysis](/analytics/funnels) * **Retention** — [cohort retention](/analytics/retention) matrix ## Note blocks **Note blocks** hold markdown text. Use them for context: explain why a metric matters, note a recent deploy, or link to related resources. Up to 50 note blocks per board. ## Pinning boards Pin a board to keep it at the top of your workspace's board list. Pinned boards are what your team sees first — use them for the dashboards everyone checks daily. ```bash theme={null} curl -X PUT https://your-tell-server/api/v1/boards/{board-id}/pin \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{"pinned": true}' ``` ## Sharing boards Share a board with anyone via a public URL — no login required. This is useful for embedding in wikis or sending to stakeholders who don't have Tell accounts. ```bash theme={null} curl -X POST https://your-tell-server/api/v1/boards/{board-id}/share \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` The response includes a URL like `https://your-tell-server/s/b/a1b2c3d4`. Revoke the link at any time to cut off access. See [Sharing](/api/sharing) for the full API reference. ## Saved metrics If you find yourself building the same metric block on multiple boards, save it. Saved metrics store the query configuration so you can reuse it or share it independently. ```bash theme={null} curl -X POST https://your-tell-server/api/v1/metrics/saved \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{ "title": "Weekly Active Users", "workspace_id": "ws_abc123", "query": { "metric": "wau", "range": "30d", "granularity": "daily" } }' ``` Saved metrics can also be shared via public links, just like boards. ## Marks Marks are timeline annotations that overlay on your charts. Use them to record deploys, incidents, launches, or any event that might explain a change in your metrics. When you see a spike or dip, marks tell you what happened. ### Create a mark From the CLI: ```bash theme={null} # Mark a release tell mark "Shipped v2.1.0" # Mark an incident with a specific time tell mark "Payments down" -c incident --at 2025-06-15T23:47:00Z # Mark with a description and reference URL tell mark "Series A announced" -c news -d "Blog post went live" --source_ref https://blog.example.com/series-a ``` Via the API: ```bash theme={null} curl -X POST https://your-tell-server/api/v1/marks \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{ "title": "Shipped v2.1.0", "category": "release", "source_ref": "https://github.com/example/repo/releases/tag/v2.1.0" }' ``` ### Categories | Category | When to use | | - | - | | `release` | Deploys, version bumps, feature launches (default) | | `news` | Blog posts, announcements, funding rounds | | `incident` | Outages, degradations, on-call events | ### Scope Marks are **public** by default — visible to everyone in the workspace. Set a mark to **private** and only you can see it: ```bash theme={null} tell mark "Testing hypothesis" --private ``` ### List and filter marks ```bash theme={null} # List marks from the last 90 days tell mark list # Filter by category and time tell mark list --last 30d -c release ``` ### Delete a mark ```bash theme={null} tell mark delete ``` Only the mark creator or a workspace admin can delete marks. See the [marks API reference](/api/queries#marks) for full endpoint documentation. ## Who can do what | Action | Role | | - | - | | View boards | Viewer | | Create boards | Editor | | Edit or delete own boards | Editor | | Edit or delete any board | Admin | | Share boards | Owner or Admin | | Create marks | Editor | | Delete own marks | Editor | | Delete any mark | Admin | ## What's next * [Boards & Metrics API](/api/boards) — full API reference * [Data Queries](/api/queries) — the metrics that power board blocks * [Segments](/analytics/segments) — filter analytics by user segments # Events Source: https://docs.tell.rs/analytics/events Analyze event counts, top events, property breakdowns, and custom aggregations. Events are the core of product analytics. When a user signs up, views a page, or makes a purchase, Tell records it as an event with a name and properties. The analytics engine lets you count events, rank them, break them down by property values, and run aggregations like sum and average. ## Quick start ```bash theme={null} # Total event count over 30 days tell metrics events --range 30d # Top 10 most frequent events tell metrics events --top --range 30d --limit 10 ``` Via the API: ```bash theme={null} curl "https://your-tell-server/api/v1/metrics/events?range=30d&granularity=daily" \ -H "Authorization: Bearer $TOKEN" ``` ## Event counts Count all events or filter to a specific event name: ```bash theme={null} # All events over time tell metrics events --range 7d # Just sign_up events tell metrics events --range 7d --event sign_up ``` API: ```bash theme={null} # Count a specific event curl "https://your-tell-server/api/v1/metrics/events?range=7d&event=sign_up" \ -H "Authorization: Bearer $TOKEN" ``` ## Top events See which events happen most often: ```bash theme={null} curl "https://your-tell-server/api/v1/metrics/events/top?range=30d&limit=10" \ -H "Authorization: Bearer $TOKEN" ``` Returns event names ranked by count, so you can quickly see what's driving volume. ## Property breakdowns Break down a specific event by its properties to understand the distribution: ```bash theme={null} # sign_up events broken down by plan property curl "https://your-tell-server/api/v1/metrics/events/properties?range=30d&event=sign_up&property=properties.plan" \ -H "Authorization: Bearer $TOKEN" ``` This returns time series data grouped by each property value (e.g., "free", "pro", "enterprise"). For top-level fields, use the field name directly (`device_type`, `country`). For custom properties stored in the JSON `properties` column, prefix with `properties.` (e.g., `properties.plan`, `properties.amount`). ## Custom aggregations Run sum, average, min, max, or count on numeric event properties: ```bash theme={null} # Average order amount for purchase events curl "https://your-tell-server/api/v1/metrics/events/custom?\ event=purchase&property=properties.amount&metric=avg&range=30d" \ -H "Authorization: Bearer $TOKEN" # Total revenue curl "https://your-tell-server/api/v1/metrics/events/custom?\ event=purchase&property=properties.amount&metric=sum&range=30d" \ -H "Authorization: Bearer $TOKEN" ``` Supported aggregation functions: `sum`, `avg`, `min`, `max`, `count`. ## First-time events Count users who did an event for the first time within a period. This answers questions like "how many users signed up for the first time this week?" — distinct from total event counts, which include repeat actions. ```bash theme={null} curl "https://your-tell-server/api/v1/metrics/events/first-time?event=sign_up&range=30d&granularity=daily" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` Tell scans each user's full event history to find their earliest occurrence, then counts only those whose first occurrence falls within the query range. The result is a time series of new user acquisition per period. ### First-time vs first-match Two modes control how filter conditions interact with the historical scan: | Mode | `match_mode` | Behavior | | - | - | - | | **first\_time** | `false` (default) | Finds user's first occurrence of the event, regardless of conditions. Conditions only filter the output range. | | **first\_match** | `true` | Finds user's first occurrence that matches ALL conditions. A user who did `purchase` before but never with `amount > 100` counts as first-time for that condition. | **first\_time** (default) — "How many users made their first purchase this month?" ```bash theme={null} curl "https://your-tell-server/api/v1/metrics/events/first-time?event=purchase&range=30d" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` **first\_match** — "How many users made their first purchase over \$100 this month?" ```bash theme={null} curl "https://your-tell-server/api/v1/metrics/events/first-time?\ event=purchase&match_mode=true&range=30d&\ conditions[0][field]=properties.amount&conditions[0][op]=gt&conditions[0][value]=100" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` ### Lookback window By default, Tell scans the entire event history to find true first occurrences. For large datasets, limit the scan with `lookback_days`: ```bash theme={null} # Only scan the last 365 days of history curl "https://your-tell-server/api/v1/metrics/events/first-time?\ event=sign_up&range=30d&lookback_days=365" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` This trades accuracy for speed — a user who first did the event 400 days ago would be miscounted as first-time if their next occurrence falls within the range. Use this when you know your product's age or only care about recent cohorts. First-time events support [breakdowns](/analytics/filtering), [comparison](/analytics/filtering), and [audience filtering](/analytics/audiences). When combined with a breakdown, Tell captures the property value at the time of the first event using `argMin`. See the [first-time events API reference](/api/queries#first-time-events) for full endpoint documentation. ## Per-actor metrics Regular event counts tell you totals. Per-actor metrics tell you about user behavior — how many events does a typical user trigger? What does the 90th percentile look like? Tell computes this with a two-level aggregation: first per user, then across users. ```bash theme={null} # Average page views per user, daily over 30 days curl "https://your-tell-server/api/v1/metrics/events/per-actor?\ event=page_view&metric=avg&range=30d&granularity=daily" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` This returns a time series where each point is the average number of `page_view` events per user for that day — not the total count, but the per-user average. ### Aggregation functions | Function | Description | | - | - | | `avg` | Average per user (default) | | `median` / `p50` | Median per user (robust to outliers) | | `p75` | 75th percentile | | `p90` | 90th percentile — identifies power users | | `p95` | 95th percentile | | `p99` | 99th percentile — extreme behavior | | `min` | Minimum per-user value | | `max` | Maximum per-user value | ### Property aggregation By default, per-actor counts events. Add `property` to aggregate a numeric property instead: ```bash theme={null} # P90 revenue per user from purchase events curl "https://your-tell-server/api/v1/metrics/events/per-actor?\ event=purchase&metric=p90&property=properties.amount&range=30d" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` Without `property`: inner aggregation is `COUNT(*)` per user (event count). With `property`: inner aggregation is `SUM(property)` per user (property total). The outer function (`avg`, `p90`, etc.) then operates on those per-user values. ### How it differs from other metrics * **Event counts** — total events across all users. "50,000 purchases happened." * **Per-actor metrics** — distribution across users. "The average user made 2.3 purchases. The p90 user made 8." * **Custom aggregations** — single-level property aggregation. "Average purchase amount is \$42." (not per-user) * **Grouped metrics** — per-actor logic applied per group for treemap visualization Per-actor metrics support [breakdowns](/analytics/filtering), all [granularity levels](/analytics/filtering), and [audience filtering](/analytics/audiences). See the [per-actor API reference](/api/queries#per-actor-metrics) for full endpoint documentation. ## Drill-down Inspect raw event data when you need to see individual records: ```bash theme={null} curl "https://your-tell-server/api/v1/metrics/events/raw?range=7d&limit=50" \ -H "Authorization: Bearer $TOKEN" ``` Returns rows with `timestamp`, `event_name`, `device_id`, and `session_id`. Add [filter conditions](/analytics/filtering) to narrow down the results. ## Formulas Combine multiple metric series with math. Define series A, B, C (up to 26), then write an expression: ```bash theme={null} curl -X POST https://your-tell-server/api/v1/metrics/formula \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{ "series": [ { "metric": "event:sign_up" }, { "metric": "event:purchase" } ], "formula": "B / A * 100", "range": "30d", "granularity": "daily" }' ``` This computes the daily conversion rate from sign-up to purchase. Series A is sign-up count, series B is purchase count, and the formula `B / A * 100` gives a percentage. ### Series references Each series maps to a letter: the first is `A`, the second is `B`, and so on up to `Z`. Reference them in your formula by letter. Available series metrics: `dau`, `wau`, `mau`, `events`, `event:`, `sessions`, `users`, `logs`. ### Operators | Operator | Description | Example | | - | - | - | | `+` | Add | `A + B` | | `-` | Subtract | `A - B` | | `*` | Multiply | `A * B` | | `/` | Divide | `A / B` | | `%` | Modulo | `A % B` | | `**` | Power | `A ** 2` | | `-` (unary) | Negate | `-A` | Standard math precedence applies. Use parentheses to override: `(A + B) / C`. Division by zero returns `0` instead of an error. ### Evaluation modes Formulas evaluate in two modes depending on context: * **Time series** — per-bucket evaluation, used for charts. Missing values default to `0`. * **Total** — evaluates on aggregate totals, used for KPI number blocks on [boards](/analytics/boards). ### Formulas on boards Add formulas to any metric [block](/analytics/boards) on a board. Define your series, write the expression, and the board evaluates it on every refresh. Use this for computed metrics like conversion rates, ratios, or derived KPIs. See the [formula API reference](/api/queries#formulas) for full endpoint documentation. ## Grouped metrics Grouped metrics show proportional composition — which countries, events, or plans make up your traffic, and how engaged each group is. The result powers treemap visualizations on [boards](/analytics/boards). Each group has two values: * **Size** — determines area (event count, unique users, or sum of a property) * **Intensity** — determines color (average events per user, percentile, or average of a property) ```bash theme={null} curl -X POST https://your-tell-server/api/v1/metrics/grouped \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{ "group_by": "country", "size": { "type": "unique_actors" }, "intensity": { "type": "avg_per_actor" }, "range": "30d" }' ``` This returns groups ordered by size, each with a size value (user count) and intensity value (average events per user). The US might have 1,500 users averaging 3.2 events each, while Canada has 800 users averaging 2.8. ### Size metrics | Type | Description | | - | - | | `count` | Total event count per group | | `unique_actors` | Distinct users per group | | `sum` | Sum of a numeric property (requires `property` field, e.g., `properties.revenue`) | ### Intensity metrics | Type | Description | | - | - | | `avg_per_actor` | Average events per user | | `percentile_per_actor` | Percentile of events per user (default: p90, set `percentile` to override) | | `unique_actors` | Distinct users (useful when size is `count`) | | `avg` | Average of a numeric property (requires `property` field) | ### Options | Field | Default | Description | | - | - | - | | `group_by` | *(required)* | Field to group by: `country`, `event_name`, `device_type`, or any `properties.*` field | | `event` | *(all events)* | Filter to a specific event | | `max_groups` | `20` | Maximum number of groups returned | | `range` | `30d` | Time range | Unlike [breakdowns](/analytics/filtering#breakdowns), grouped metrics always return a snapshot (not a time series) and use two-level aggregation — first per user, then per group — to reveal engagement patterns, not just raw counts. See the [grouped metrics API reference](/api/queries#grouped-metrics) for full endpoint documentation. ## Lifecycle The lifecycle metric tracks how your active user base evolves over time. Each period, Tell classifies every active user into one of three states: | State | Meaning | | - | - | | **new** | First ever activity falls within this period | | **returning** | Active in both this period and the previous one | | **resurrected** | Was inactive in the previous period, came back | ```bash theme={null} curl "https://your-tell-server/api/v1/metrics/lifecycle?range=30d&granularity=weekly" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` Returns a time series with one point per state per period: ```json theme={null} { "points": [ {"date": "2025-02-03", "value": 150, "dimension": "new"}, {"date": "2025-02-03", "value": 800, "dimension": "returning"}, {"date": "2025-02-03", "value": 50, "dimension": "resurrected"}, {"date": "2025-02-10", "value": 200, "dimension": "new"}, {"date": "2025-02-10", "value": 750, "dimension": "returning"}, {"date": "2025-02-10", "value": 80, "dimension": "resurrected"} ], "total": 2030 } ``` A healthy product has a large returning base with steady new user inflow. If resurrected starts outpacing new, your acquisition may be slowing while re-engagement improves. If returning shrinks, users aren't sticking around. The default granularity is `weekly`. Supports `daily`, `monthly`, and all other [granularity levels](/analytics/filtering). Add `compare=previous` for period-over-period comparison. This is different from [lifecycle segments](/analytics/segments#lifecycle-segments). The lifecycle metric is a **time series** — it classifies users per period with relative lookback. Segments are a **snapshot** — they classify all users right now using fixed 7/30-day windows and include `dormant` and `churned` states. See the [lifecycle API reference](/api/queries#lifecycle) for full endpoint documentation. ## Breakdowns and comparison Event metrics support the full [filtering system](/analytics/filtering) — breakdowns by any field, period comparison, and all granularity levels. ```bash theme={null} # Event count by device type, compared to previous period tell metrics events --range 30d --breakdown device_type --compare previous ``` # Filtering & Breakdowns Source: https://docs.tell.rs/analytics/filtering Apply conditions with 15 operators, break down by any field, compare periods, and control time granularity. Every Tell metric supports the same filtering system. You can narrow results with conditions, split them by a dimension, change the time granularity, and compare against a previous period — all using the same parameters across the CLI, API, and TUI. ## Time ranges Tell supports relative ranges, predefined periods, and custom date ranges. **Relative ranges** — look back from today: | Range | Meaning | | - | - | | `7d` | Last 7 days | | `30d` | Last 30 days | | `90d` | Last 90 days | | `3m`, `6m`, `12m` | Last 3, 6, or 12 calendar months | | `1h`, `24h` | Last 1 or 24 hours | **Predefined periods:** | Range | Meaning | | - | - | | `today` | Today | | `yesterday` | Yesterday | | `wtd` | Week to date | | `mtd` | Month to date | | `qtd` | Quarter to date | | `ytd` | Year to date | | `last_week` | Previous complete week | | `last_month` | Previous complete month | | `last_quarter` | Previous complete quarter | | `last_year` | Previous complete year | | `all_time` | All available data | **Custom date range** — comma-separated start and end: ``` 2024-01-01,2024-03-31 ``` ## Granularity Control how data is bucketed in time. The default is `daily`. | Granularity | Aliases | | - | - | | `minute` | `min`, `1m` | | `hourly` | `hour`, `1h` | | `daily` | `day`, `1d` | | `weekly` | `week`, `1w` | | `monthly` | `month` | | `quarterly` | `quarter` | | `yearly` | `year`, `1y` | ```bash theme={null} # Hourly DAU for today tell metrics dau --range today --granularity hourly # Monthly event counts for the year tell metrics events --range ytd --granularity monthly ``` Omit granularity to get a rolling window — a single aggregate value instead of a time series. ## Filter conditions Add conditions to narrow down which data is included. Each condition has a field, operator, and value. ### Operators | Operator | Aliases | Description | | - | - | - | | `eq` | `=`, `==` | Equal | | `ne` | `!=`, `<>` | Not equal | | `gt` | `>` | Greater than | | `gte` | `>=` | Greater than or equal | | `lt` | `<` | Less than | | `lte` | `<=` | Less than or equal | | `contains` | `like` | Contains substring | | `not_contains` | `not_like` | Does not contain substring | | `starts_with` | | Starts with | | `ends_with` | | Ends with | | `in` | | In list of values | | `not_in` | | Not in list | | `is_set` | `isset` | Field is not null | | `is_not_set` | `isnotset` | Field is null | | `regex` | `~` | Regex match | ### API format Pass conditions as query parameters: ```bash theme={null} # Filter to mobile users in the US curl "https://your-tell-server/api/v1/metrics/dau?range=30d\ &conditions[0][field]=device_type&conditions[0][operator]=eq&conditions[0][value]=mobile\ &conditions[1][field]=country&conditions[1][operator]=eq&conditions[1][value]=US" \ -H "Authorization: Bearer $TOKEN" ``` ### JSON properties Filter on custom event properties using dot notation. Properties stored in the JSON `properties` column are accessed as `properties.field_name`: ``` conditions[0][field]=properties.plan&conditions[0][operator]=eq&conditions[0][value]=pro ``` Nested paths work too: `properties.user.role`. ## Breakdowns Add a breakdown to split any metric by a dimension: ```bash theme={null} # DAU by country tell metrics dau --range 30d --breakdown country # Events by event name tell metrics events --range 7d --breakdown event_name ``` The response returns data grouped by each dimension value. Common breakdown fields: * `device_type` — desktop, mobile, tablet * `country` — country code * `operating_system` — OS name * `event_name` — event type * `level` — log level (for log metrics) * `source` — data source * Any `properties.*` field ## Comparison Compare the current period to a previous one: | Mode | Aliases | Compares to | | - | - | - | | `previous` | `prev`, `previous_period` | Same duration, immediately before | | `previous_year` | `yoy`, `year_over_year` | Same dates, one year earlier | ```bash theme={null} # This month vs last month tell metrics dau --range 30d --compare previous # Year-over-year MAU tell metrics mau --range 90d --compare yoy ``` The response includes a `comparison` object with `previous_total`, `change` (absolute), `percent_change`, and optionally the previous period's data points. ## Limits Results are capped at 10,000 rows by default. For raw drill-down queries, set `limit` explicitly: ```bash theme={null} curl "https://your-tell-server/api/v1/metrics/events/raw?range=7d&limit=100" \ -H "Authorization: Bearer $TOKEN" ``` ## Audience filtering Scope any metric to a saved [audience](/analytics/audiences). Audiences are reusable user groups defined by property filters and behavioral rules — "US users who purchased in the last 30 days", for example. When an audience is applied, Tell resolves the rules to a set of matching device IDs and restricts the metric query to those users. This works across all metric types: active users, events, [funnels](/analytics/funnels), retention, lifecycle, and stickiness. # Funnels Source: https://docs.tell.rs/analytics/funnels Measure step-by-step conversion — see where users drop off between key events. Funnels track how users move through a sequence of events. Define the steps (sign up, add to cart, purchase), set a conversion window, and Tell shows you how many users completed each step, where they dropped off, and overall conversion rate. ## Analyze a funnel Send a POST request with your steps and a conversion window: ```bash theme={null} curl -X POST https://your-tell-server/api/v1/funnels \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{ "steps": [ { "event": "sign_up" }, { "event": "add_to_cart" }, { "event": "purchase" } ], "window_seconds": 604800, "range": "30d" }' ``` Response: ```json theme={null} { "steps": [ { "step": 1, "event": "sign_up", "count": 1200, "overall_rate": 1.0, "step_rate": 1.0, "drop_off": 0 }, { "step": 2, "event": "add_to_cart", "count": 480, "overall_rate": 0.4, "step_rate": 0.4, "drop_off": 720 }, { "step": 3, "event": "purchase", "count": 192, "overall_rate": 0.16, "step_rate": 0.4, "drop_off": 288 } ], "overall_conversion": 0.16 } ``` Each step shows: * **count** — users who reached this step * **overall\_rate** — conversion from step 1 (0.0 to 1.0) * **step\_rate** — conversion from the previous step * **drop\_off** — users lost between this step and the previous one ## Steps and filters A funnel needs 2–10 steps. Each step is an event name, optionally with property filters: ```json theme={null} { "steps": [ { "event": "sign_up" }, { "event": "add_to_cart", "filters": [ { "field": "properties.category", "operator": "eq", "value": "electronics" } ] }, { "event": "purchase" } ], "window_seconds": 604800, "range": "30d" } ``` This funnel only counts `add_to_cart` events where the `category` property equals `"electronics"`. The other steps accept any matching event. ### Step filter operators | Operator | Aliases | Description | | - | - | - | | `eq` | `=`, `==` | Equal | | `ne` | `!=`, `<>` | Not equal | | `gt` | `>` | Greater than | | `gte` | `>=` | Greater than or equal | | `lt` | `<` | Less than | | `lte` | `<=` | Less than or equal | | `contains` | `like` | Contains substring | | `not_contains` | `not_like` | Does not contain | | `starts_with` | | Starts with | | `ends_with` | | Ends with | | `in` | | Value is in list | | `not_in` | | Value is not in list | | `is_set` | `isset` | Field is not null | | `is_not_set` | `isnotset` | Field is null | | `regex` | `~` | Regex match | For custom properties, prefix the field with `properties.` (e.g., `properties.plan`). Top-level fields like `country` or `device_type` use the field name directly. ## Conversion window The `window_seconds` parameter sets the maximum time a user has to complete the entire funnel from their first step. Default is `604800` (7 days). A user who triggers step 1 on Monday and step 3 on the following Sunday (within 7 days) counts as converted. A user who takes 8 days does not. ```json theme={null} { "steps": [ { "event": "trial_start" }, { "event": "subscription_created" } ], "window_seconds": 1209600, "range": "90d" } ``` This gives users a 14-day window to convert from trial to subscription. ## Breakdowns Add a `breakdown` field to split funnel results by a dimension: ```bash theme={null} curl -X POST https://your-tell-server/api/v1/funnels \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{ "steps": [ { "event": "sign_up" }, { "event": "purchase" } ], "window_seconds": 604800, "breakdown": "country", "range": "30d" }' ``` Response includes a `breakdown` array, each group with its own step results and conversion rate: ```json theme={null} { "steps": [ ... ], "overall_conversion": 0.16, "breakdown": [ { "dimension": "US", "steps": [ { "step": 1, "event": "sign_up", "count": 600, "overall_rate": 1.0, "step_rate": 1.0, "drop_off": 0 }, { "step": 2, "event": "purchase", "count": 120, "overall_rate": 0.2, "step_rate": 0.2, "drop_off": 480 } ], "overall_conversion": 0.2 }, { "dimension": "DE", "steps": [ { "step": 1, "event": "sign_up", "count": 300, "overall_rate": 1.0, "step_rate": 1.0, "drop_off": 0 }, { "step": 2, "event": "purchase", "count": 36, "overall_rate": 0.12, "step_rate": 0.12, "drop_off": 264 } ], "overall_conversion": 0.12 } ] } ``` Use breakdowns to compare conversion across countries, device types, plans, or any property. ## Funnel trends Track how conversion changes over time with the trend endpoint: ```bash theme={null} curl -X POST https://your-tell-server/api/v1/funnels/trend \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{ "steps": [ { "event": "sign_up" }, { "event": "purchase" } ], "window_seconds": 604800, "range": "30d", "granularity": "daily" }' ``` Returns time series data with the overall conversion rate bucketed by the requested granularity. Use this to spot trends — is your onboarding funnel improving week over week? Supported granularity values: `minute`, `hourly`, `daily`, `weekly`, `monthly`, `quarterly`, `yearly`. ## What's next * [Filtering](/analytics/filtering) — apply conditions, breakdowns, and comparisons to any metric * [Events](/analytics/events) — explore the events that power your funnel steps * [Boards](/analytics/boards) — save funnel results to a board for ongoing monitoring * [Data queries API](/api/queries) — full API reference for funnels and other metrics # Analytics Overview Source: https://docs.tell.rs/analytics/overview How Tell turns raw events and logs into answers. Tell collects events, sessions, and logs from your app — then lets you query them as metrics, time series, and breakdowns. You get answers like "how many daily active users this week?" or "which events are most common?" without writing SQL. ## What you can measure | Category | Metrics | Learn more | | - | - | - | | **Visitors** | DAU, WAU, MAU, user count, location, device, OS | [Visitors & Page Views](/analytics/page-views) | | **Sessions** | Session volume, unique sessions, stickiness (DAU/MAU) | [Sessions & Stickiness](/analytics/sessions) | | **Events** | Event counts, top events, property breakdowns, aggregations | [Events](/analytics/events) | | **Logs** | Log volume by level, top logs by source/service | Part of the metrics API | | **Segments** | Lifecycle (new, returning, dormant, churned) and engagement segments | [Segments](/analytics/segments) | | **Predictions** | Churn risk, auto-segmentation, anomaly detection, forecasting | [Predictions](/analytics/predictions/overview) | ## How it works Every metric follows the same pattern: pick a time range, optionally add filters and breakdowns, and get back a time series. ```bash theme={null} # Daily active users over the last 30 days tell metrics dau --range 30d # Event counts broken down by device type tell metrics events --range 7d --breakdown device_type # Compare this week's sessions to last week tell metrics sessions --range 7d --compare previous ``` The same queries work through the [HTTP API](/api/queries), the [TUI](/tools/tui), and [boards](/analytics/boards). ## Filtering and breakdowns All metrics support the same filtering system — 15 operators, breakdowns by any field, time granularity from minute to yearly, and period comparison. See [Filtering & Breakdowns](/analytics/filtering) for the full reference. ## Boards Combine multiple metrics into dashboards with [Boards](/analytics/boards). Each board contains metric blocks that you configure with their own time ranges, filters, and visualizations. ## SQL access For queries that go beyond built-in metrics, use [raw SQL](/analytics/sql) directly against ClickHouse — scoped to your workspace and security-validated. # Visitors & Page Views Source: https://docs.tell.rs/analytics/page-views Track unique visitors with DAU, WAU, and MAU — broken down by device, country, or any field. Know how many people use your product each day, week, or month. Tell counts unique visitors (by device ID) and gives you DAU, WAU, and MAU out of the box. ## Quick start ```bash theme={null} # Daily active users, last 30 days tell metrics dau --range 30d # Weekly active users, broken down by country tell metrics wau --range 90d --breakdown country ``` Or via the API: ```bash theme={null} curl "https://your-tell-server/api/v1/metrics/dau?range=30d" \ -H "Authorization: Bearer $TOKEN" ``` ## Metrics **DAU** (daily active users) — distinct visitors per day. If a user visits three times in one day, they count once. **WAU** (weekly active users) — distinct visitors per rolling 7-day window. **MAU** (monthly active users) — distinct visitors per rolling 30-day window. **User count** — total user records over time from the users table, distinct from active user metrics. ## Breakdowns Split any visitor metric by a dimension to see where your users come from: ```bash theme={null} # Visitors by device type (desktop, mobile, tablet) tell metrics dau --range 30d --breakdown device_type # Visitors by operating system tell metrics dau --range 30d --breakdown operating_system # Visitors by country tell metrics dau --range 30d --breakdown country ``` Via the API, add `&breakdown=device_type` to any active user endpoint. ## Comparing periods See how visitor numbers changed compared to the previous period or same period last year: ```bash theme={null} # DAU this month vs last month tell metrics dau --range 30d --compare previous # Year-over-year comparison tell metrics mau --range 90d --compare yoy ``` The response includes the current total, previous total, absolute change, and percent change. ## Drill-down When you see a spike or drop in visitors, drill into the raw data to see which specific users were active: ```bash theme={null} curl "https://your-tell-server/api/v1/metrics/dau/raw?range=7d&limit=50" \ -H "Authorization: Bearer $TOKEN" ``` This returns individual device IDs within the time range, so you can investigate further. ## Time ranges and granularity All visitor metrics support the full set of [time ranges and granularity options](/analytics/filtering) — from minute-level resolution up to yearly buckets, with predefined ranges like `today`, `wtd`, `mtd`, `ytd`, or custom date ranges. # Anomaly Detection Source: https://docs.tell.rs/analytics/predictions/anomaly Isolation forest for log anomaly scoring. Isolation forest for log anomaly scoring. # Churn Prediction Source: https://docs.tell.rs/analytics/predictions/churn Logistic regression scoring — P(churn) and P(complete) per user. Logistic regression scoring — P(churn) and P(complete) per user. # Forecasting Source: https://docs.tell.rs/analytics/predictions/forecasting Holt-Winters triple exponential smoothing with confidence intervals. Holt-Winters triple exponential smoothing with confidence intervals. # Predictions Overview Source: https://docs.tell.rs/analytics/predictions/overview Real-time ML predictions in the pipeline — churn, segmentation, anomalies, forecasting. Real-time ML predictions in the pipeline — churn, segmentation, anomalies, forecasting. Sub-microsecond enrichment. # Auto Segmentation Source: https://docs.tell.rs/analytics/predictions/segmentation K-means clustering with auto-generated segment names. K-means clustering with auto-generated segment names. # Retention Source: https://docs.tell.rs/analytics/retention Measure how many users come back after their first visit — cohort by cohort. Retention shows whether users stick around. Tell groups users into cohorts by when they first appeared, then tracks what percentage returns in each subsequent period. The result is a cohort matrix — rows are cohorts, columns are time periods, cells are retention rates. ## Run a retention analysis ```bash theme={null} curl -X POST https://your-tell-server/api/v1/retention \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{ "granularity": "weekly", "periods": 8, "range": "90d" }' ``` Response: ```json theme={null} { "cohorts": [ { "cohort_date": "2025-04-07", "cohort_size": 320, "retention": [1.0, 0.45, 0.32, 0.28, 0.25, 0.22, 0.20, 0.19], "counts": [320, 144, 102, 90, 80, 70, 64, 61] }, { "cohort_date": "2025-04-14", "cohort_size": 280, "retention": [1.0, 0.50, 0.36, 0.30, 0.27, 0.24, 0.21], "counts": [280, 140, 101, 84, 76, 67, 59] } ], "averages": [1.0, 0.47, 0.34, 0.29, 0.26, 0.23, 0.21, 0.19], "granularity": "week", "periods": 8 } ``` Each cohort shows: * **cohort\_date** — the start of the period when these users first appeared * **cohort\_size** — how many distinct users entered in this cohort * **retention** — retention rate per period (0.0 to 1.0), where period 0 is always 1.0 * **counts** — absolute user counts per period The **averages** array gives the mean retention rate across all cohorts for each period offset. ## Granularity Control how cohorts and periods are bucketed: | Granularity | Aliases | Cohort grouping | | - | - | - | | `daily` | `day` | Users grouped by the day they first appeared | | `weekly` | `week` | Users grouped by the week they first appeared (default) | | `monthly` | `month` | Users grouped by the month they first appeared | Weekly retention is the most common starting point. Use daily for short-term analysis (onboarding flows) and monthly for long-term trends. ## Periods The `periods` parameter controls how many subsequent time periods to track. Default is `8`, maximum is `52`. ```json theme={null} { "granularity": "monthly", "periods": 12, "range": "365d" } ``` This tracks 12 months of retention — enough to see a full year of cohort behavior. ## Start and return events By default, Tell counts any event as both the cohort entry and the return activity. Use `start_event` and `return_event` to be specific: ```bash theme={null} curl -X POST https://your-tell-server/api/v1/retention \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{ "granularity": "weekly", "periods": 8, "start_event": "sign_up", "return_event": "purchase", "range": "90d" }' ``` This answers: "Of users who signed up each week, what percentage made a purchase in subsequent weeks?" Useful for measuring activation, not just return visits. | Field | Default | Description | | - | - | - | | `start_event` | any event | Event that defines cohort entry | | `return_event` | any event | Event that counts as a return | ## Request parameters | Field | Type | Default | Description | | - | - | - | - | | `granularity` | string | `weekly` | Cohort bucketing: `daily`, `weekly`, `monthly` | | `periods` | integer | `8` | Number of periods to track (1–52) | | `start_event` | string | *(any event)* | Cohort entry event | | `return_event` | string | *(any event)* | Return activity event | | `range` | string | `90d` | Time range for the analysis | ## Reading the cohort matrix The retention matrix is a triangle. Earlier cohorts have more columns (more time has passed), later cohorts have fewer. Here's how to read it: * **Row**: one cohort — all users who first appeared in that period * **Column 0**: always 100% (the cohort itself) * **Column N**: percentage of the cohort that returned N periods later * **Averages row**: mean retention across all cohorts for each column A healthy product shows retention curves that flatten — users who survive the first few periods tend to stay. If curves keep declining, you have a leaky bucket. ## Retention on boards Retention has a dedicated [board](/analytics/boards) visualization type. Add a retention block to any board to track cohort retention over time. The block uses the same parameters: granularity, periods, and optional start/return events. ## What's next * [Audiences](/analytics/audiences) — scope retention to a specific user group * [Funnels](/analytics/funnels) — measure step-by-step conversion alongside retention * [Segments](/analytics/segments) — lifecycle segments that complement retention analysis * [Data queries API](/api/queries) — full API reference for the retention endpoint # Segments Source: https://docs.tell.rs/analytics/segments Built-in lifecycle and engagement segments with user counts and drill-down. Tell automatically classifies your users into segments based on their behavior — no configuration needed. You get lifecycle segments (is this user new or churned?) and engagement segments (is this a power user or a one-timer?) computed at query time from your actual data. ## Built-in segments ### Lifecycle segments Point-in-time classification based on when a user was first and last seen: | Segment | Definition | | - | - | | **new** | First seen within the last 7 days | | **returning** | Not new, but active in the last 7 days | | **dormant** | Last seen 8-30 days ago | | **churned** | Last seen more than 30 days ago | ### Engagement segments Based on total session count (from the events table): | Segment | Definition | | - | - | | **one\_timer** | Only 1 session total | | **casual** | 2-4 sessions | | **regular** | 5-14 sessions | | **power\_user** | 15+ sessions | ## Segment counts Get the number of users in each segment: ```bash theme={null} curl "https://your-tell-server/api/v1/metrics/segments?range=30d" \ -H "Authorization: Bearer $TOKEN" ``` Returns all 8 segments with their user counts and category: ```json theme={null} [ { "segment": "new", "category": "lifecycle", "users": 342 }, { "segment": "returning", "category": "lifecycle", "users": 1205 }, { "segment": "dormant", "category": "lifecycle", "users": 587 }, { "segment": "churned", "category": "lifecycle", "users": 2341 }, { "segment": "one_timer", "category": "engagement", "users": 891 }, { "segment": "casual", "category": "engagement", "users": 645 }, { "segment": "regular", "category": "engagement", "users": 412 }, { "segment": "power_user", "category": "engagement", "users": 189 } ] ``` Lifecycle and engagement counts are computed independently — a user can be both "returning" and "power\_user". ## Segment drill-down See which users belong to a specific segment: ```bash theme={null} # List power users with their stats curl "https://your-tell-server/api/v1/metrics/segments/power_user?range=30d&limit=20" \ -H "Authorization: Bearer $TOKEN" ``` For lifecycle segments, the response includes `first_seen` and `last_seen` timestamps. For engagement segments, it includes `session_count`. ```json theme={null} [ { "device_id": "abc123", "segment": "power_user", "session_count": 47 } ] ``` ## How segments are computed Segments are calculated at query time — not pre-computed. Tell runs a single query using your filter's time range to classify users. This means segment counts always reflect the current state of your data within the selected time range. No ML is involved in these segments. For ML-based segmentation (auto-generated clusters and churn predictions), see [Predictions](/analytics/predictions/overview). Lifecycle segments are a **snapshot** — they classify all users right now using fixed 7/30-day windows. For a **time series** view of how new, returning, and resurrected users change over time, see the [lifecycle metric](/analytics/events#lifecycle). ## Use cases **Spotting churn early.** Check the dormant segment regularly. If it's growing faster than new users are arriving, you have a retention problem. **Measuring engagement quality.** A healthy product has more regular and power users than one-timers. Track the ratio over time. **Targeting re-engagement.** Drill into the dormant or churned segments to find users who were once active. Combine with the [events](/analytics/events) data to understand what they used to do. # Sessions & Stickiness Source: https://docs.tell.rs/analytics/sessions Session volume, unique sessions, and engagement ratios — DAU/MAU and WAU/MAU. Sessions tell you how often people come back. Stickiness tells you what fraction of your monthly audience is active on any given day or week — a direct measure of engagement. ## Quick start ```bash theme={null} # Session volume over the last 30 days tell metrics sessions --range 30d # Daily stickiness (DAU/MAU ratio) tell metrics stickiness --range 30d --type daily ``` ## Session metrics **Session volume** — total session count over time. Each session record counts as one. **Unique sessions** — distinct session IDs per time bucket. Filters out duplicate records for the same session. ```bash theme={null} # Unique sessions, weekly buckets tell metrics sessions --range 90d --granularity weekly --type unique ``` Via the API: ```bash theme={null} # Session volume curl "https://your-tell-server/api/v1/metrics/sessions?range=30d" \ -H "Authorization: Bearer $TOKEN" # Unique sessions curl "https://your-tell-server/api/v1/metrics/sessions/unique?range=30d" \ -H "Authorization: Bearer $TOKEN" ``` ## Top sessions See which device types, countries, or operating systems generate the most sessions: ```bash theme={null} # Top 10 device types by session count curl "https://your-tell-server/api/v1/metrics/sessions/top?range=30d&property=device_type&limit=10" \ -H "Authorization: Bearer $TOKEN" ``` Available groupings: `device_type`, `country`, `operating_system`, or any field in the context table. ## Stickiness Stickiness measures what proportion of your user base is active in a shorter window. Higher stickiness means better engagement. **Daily stickiness** = DAU / MAU. A value of 50% means half your monthly users visit every day. **Weekly stickiness** = WAU / MAU. A value of 80% means most monthly users visit at least once a week. ```bash theme={null} # Daily stickiness with weekly granularity tell metrics stickiness --range 90d --type daily --granularity weekly # Weekly stickiness tell metrics stickiness --range 90d --type weekly ``` Via the API: ```bash theme={null} curl "https://your-tell-server/api/v1/metrics/stickiness/daily?range=30d" \ -H "Authorization: Bearer $TOKEN" ``` ## Breakdowns and comparison Session metrics support the same [filtering and breakdowns](/analytics/filtering) as all other metrics — split by any field, compare to the previous period, and control time granularity. ```bash theme={null} # Sessions by country, compared to previous period tell metrics sessions --range 30d --breakdown country --compare previous ``` # SQL Queries Source: https://docs.tell.rs/analytics/sql Execute SELECT queries via API or CLI — workspace-scoped and security-validated. When built-in metrics don't cover your question, you can run SQL directly against ClickHouse. Tell scopes every query to your workspace and only allows SELECT — so you get full query power without risk of modifying data. ## Quick start From the CLI: ```bash theme={null} tell query "SELECT event_name, COUNT(*) as total FROM events_v1 GROUP BY event_name ORDER BY total DESC" ``` Or via the API: ```bash theme={null} curl -X POST "https://your-tell-server/api/v1/data/query" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: 1" \ -d '{"query": "SELECT event_name, COUNT(*) as total FROM events_v1 GROUP BY event_name ORDER BY total DESC"}' ``` You don't need to worry about workspace isolation — Tell rewrites your query to scope it automatically. ## Available tables | Table | Contents | | - | - | | `events_v1` | Product events — `timestamp`, `event_name`, `device_id`, `session_id`, `properties` (JSON) | | `context_v1` | Device and session context — `device_type`, `operating_system`, `country`, `app_version`, and more | | `logs_v1` | Application logs — `level`, `source`, `service`, `message` (JSON) | | `users_v1` | User profiles — `user_id`, `email`, `name` | | `user_traits_v1` | User properties — `user_id`, `trait_key`, `trait_value` | | `user_devices` | Device-to-user mappings — `user_id`, `device_id`, `linked_at` | | `snapshots_v1` | Integration snapshots — `source`, `entity`, `metrics` (JSON) | To discover tables and columns programmatically: ```bash theme={null} # List available tables curl "https://your-tell-server/api/v1/data/sources" \ -H "Authorization: Bearer $TOKEN" -H "X-Workspace-ID: 1" # List columns for a table curl "https://your-tell-server/api/v1/data/sources/events/fields" \ -H "Authorization: Bearer $TOKEN" -H "X-Workspace-ID: 1" # Get distinct values for a field curl "https://your-tell-server/api/v1/data/sources/events/values/event_name" \ -H "Authorization: Bearer $TOKEN" -H "X-Workspace-ID: 1" ``` ## Example queries **Events by type in the last 7 days:** ```sql theme={null} SELECT event_name, COUNT(*) as count, COUNT(DISTINCT device_id) as unique_devices FROM events_v1 WHERE timestamp > now() - INTERVAL 7 DAY GROUP BY event_name ORDER BY count DESC ``` **Error logs by service in the last 24 hours:** ```sql theme={null} SELECT service, COUNT(*) as errors FROM logs_v1 WHERE level IN ('ERROR', 'CRITICAL') AND timestamp > now() - INTERVAL 24 HOUR GROUP BY service ORDER BY errors DESC ``` **Users with multiple devices:** ```sql theme={null} SELECT u.user_id, u.email, COUNT(DISTINCT ud.device_id) as devices FROM users_v1 u JOIN user_devices ud ON u.user_id = ud.user_id GROUP BY u.user_id, u.email HAVING devices > 1 ORDER BY devices DESC ``` ## CLI output formats The `tell query` command supports three output formats: ```bash theme={null} # Table (default) — columns aligned in your terminal tell query "SELECT event_name, COUNT(*) FROM events_v1 GROUP BY event_name" # JSON — one object per row tell query --format json "SELECT event_name, COUNT(*) FROM events_v1 GROUP BY event_name" # CSV — for piping into other tools tell query --format csv "SELECT event_name, COUNT(*) FROM events_v1 GROUP BY event_name" ``` ## Security Tell validates every query before execution: * **SELECT only** — `INSERT`, `UPDATE`, `DELETE`, `DROP`, `ALTER`, and `TRUNCATE` are blocked * **Workspace scoping** — table references are rewritten to `{workspace_id}.table_name` automatically * **No multi-statement** — semicolons cannot separate multiple queries * **Rate limited** — 30 queries per minute via the API CTEs (`WITH ... AS`), subqueries, and JOINs all work normally. ## Limits | Parameter | Default | Maximum | | - | - | - | | Result rows | 1,000 | 10,000 | | Execution time | 60s | 60s | | API rate | — | 30 req/min | Set a custom limit with `LIMIT` in your query or `"limit"` in the API request body. # Authentication Source: https://docs.tell.rs/api/authentication JWT tokens for the HTTP API, streaming keys for data ingestion. Tell uses two auth systems: **JWT tokens** for the HTTP API (dashboards, queries, management) and **streaming API keys** for data ingestion (SDKs, TCP, syslog). Both are workspace-scoped. ## Quick start First-time setup creates your admin account and returns both token types: ```bash theme={null} # Check if setup is needed curl https://your-tell-server/api/v1/auth/setup/status # Create the first admin curl -X POST https://your-tell-server/api/v1/auth/setup \ -H "Content-Type: application/json" \ -d '{"email": "admin@example.com", "password": "your-password"}' ``` The setup response includes a JWT token for immediate API access and a streaming key for SDK ingestion. ## JWT tokens (HTTP API) All API requests use a `tell_`-prefixed JWT token in the Authorization header: ```bash theme={null} curl https://your-tell-server/api/v1/metrics/dau?range=30d \ -H "Authorization: Bearer tell_eyJhbG..." ``` ### Login ```bash theme={null} curl -X POST https://your-tell-server/api/v1/auth/login \ -H "Content-Type: application/json" \ -d '{"email": "you@example.com", "password": "your-password"}' ``` Returns: ```json theme={null} { "token": "tell_eyJhbG...", "user": { "id": "...", "email": "you@example.com", "role": "editor" }, "expires_at": 1728000000 } ``` Tokens expire after 24 hours by default. ### Refresh Extend a token's expiry without re-entering credentials: ```bash theme={null} curl -X POST https://your-tell-server/api/v1/auth/refresh \ -H "Content-Type: application/json" \ -d '{"token": "tell_eyJhbG..."}' ``` Returns a new token. The old token is invalidated. ### Logout ```bash theme={null} curl -X POST https://your-tell-server/api/v1/auth/logout \ -H "Authorization: Bearer tell_eyJhbG..." ``` This ends the current session and revokes the token so it can't be reused. ### Current user Check who a token belongs to: ```bash theme={null} curl https://your-tell-server/api/v1/auth/me \ -H "Authorization: Bearer tell_eyJhbG..." ``` Returns: ```json theme={null} { "id": "...", "email": "you@example.com", "role": "editor" } ``` Any authenticated user can call it, and it is not rate-limited like the other auth endpoints. A missing, invalid, or revoked token returns `401`. ### CLI login The CLI stores credentials in your OS keyring (macOS Keychain, Windows Credential Manager, or Linux Secret Service): ```bash theme={null} tell login tell logout ``` ## Streaming API keys (data ingestion) SDKs and pipeline sources use 32-character hex keys to authenticate data ingestion: ``` a1b2c3d4e5f60718293a4b5c6d7e8f90 ``` Each key maps to a workspace. You get your first streaming key during setup. Manage additional keys with the CLI: ```bash theme={null} tell apikeys create tell apikeys list tell apikeys delete ``` See [API Keys](/running-tell/api-keys) for the full reference. ## Sessions Tell tracks sessions per device with IP address and user agent. You can be logged in from multiple devices simultaneously. The `logout` endpoint terminates all sessions at once. ## Rate limiting Auth endpoints are rate-limited to **10 requests per minute** per IP address. If you exceed this, you'll receive a `429 Too Many Requests` response with a `Retry-After: 60` header. ## List responses Every endpoint that returns a list of one kind of resource uses the same shape: ```json theme={null} { "items": [ ... ], "total": 134, "limit": 50, "offset": 0, "has_more": true } ``` * `items` holds one page of results. * `total` counts every matching row across all pages, not just this page. * `limit` and `offset` echo the page you got. * `has_more` is `true` when rows exist past this page. Page with the `limit` and `offset` query parameters: | Parameter | Default | Description | | - | - | - | | `limit` | `50` | Page size. Values above `200` are clamped to `200`. | | `offset` | `0` | Rows to skip. | Boards, canvases, board groups, and workspace lists (`/boards`, `/canvases`, `/board-groups`, `/user/workspaces`, `/admin/workspaces`) default to `limit=200` so a plain request returns everything for typical workspaces. The maximum is still `200`. Some lists add fields beside these five. For example, `/people` also returns `all_time` and `total_events_scope`. Single resources and analytics results (metric series, funnels, retention) don't use this shape. ## Token format | Type | Format | Used for | | - | - | - | | JWT token | `tell_eyJhbG...` | HTTP API (queries, dashboards, management) | | Streaming key | `a1b2c3d4...` (32 hex chars) | Data ingestion (TCP, HTTP source, syslog) | JWT tokens contain the user ID, email, role, and workspace ID. Streaming keys map directly to a workspace ID with no additional claims. ## What's next * [Roles & Permissions](/running-tell/roles) — what each role can do * [API Keys](/running-tell/api-keys) — managing streaming and programmatic keys * [Workspaces](/running-tell/workspaces) — multi-tenant workspace isolation # Boards & Metrics Source: https://docs.tell.rs/api/boards CRUD operations for boards and saved metrics via the API. Boards are configurable dashboards with metric blocks and markdown notes. Saved metrics let you store and reuse metric configurations. Both are workspace-scoped and support sharing via public links. All endpoints require a JWT token and `X-Workspace-ID` header: ```bash theme={null} curl https://your-tell-server/api/v1/boards \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` ## Boards ### Create a board ```bash theme={null} curl -X POST https://your-tell-server/api/v1/boards \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{ "title": "Product Overview", "description": "Key metrics for the product team" }' ``` Requires Editor role or higher. Titles must be 1-200 characters. Descriptions are optional (max 2,000 characters). ### List boards ```bash theme={null} # All boards in workspace curl https://your-tell-server/api/v1/boards \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" # Pinned boards only curl "https://your-tell-server/api/v1/boards?pinned_only=true" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` Returns a [list response](/api/authentication#list-responses): ```json theme={null} { "items": [ { "id": "brd_x1y2", "title": "Product Overview", "description": "Key metrics for the product team", "pinned": true } ], "total": 1, "limit": 200, "offset": 0, "has_more": false } ``` The default page size is `200`. Pass `limit` and `offset` to page. ### Get a board ```bash theme={null} curl https://your-tell-server/api/v1/boards/{board-id} \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` Returns the board with its full settings, including all metric blocks and note blocks. ### Update a board ```bash theme={null} curl -X PUT https://your-tell-server/api/v1/boards/{board-id} \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{ "title": "Updated Title", "settings": { ... } }' ``` Only the board owner or an Admin can update. Settings include the grid layout, metric blocks (max 50), and note blocks (max 50). Each metric block supports up to 20 series. ### Delete a board ```bash theme={null} curl -X DELETE https://your-tell-server/api/v1/boards/{board-id} \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` ### Pin a board Pinned boards appear at the top of the board list for quick access. ```bash theme={null} curl -X PUT https://your-tell-server/api/v1/boards/{board-id}/pin \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{"pinned": true}' ``` ## Saved metrics Saved metrics store a metric configuration (query, time range, breakdown) for reuse on boards or standalone viewing. ### Create a saved metric ```bash theme={null} curl -X POST https://your-tell-server/api/v1/metrics/saved \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{ "title": "Weekly Active Users", "workspace_id": "ws_abc123", "query": { "metric": "wau", "range": "30d", "granularity": "daily" } }' ``` ### List saved metrics ```bash theme={null} curl "https://your-tell-server/api/v1/metrics/saved?workspace_id=ws_abc123" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` Returns a [list response](/api/authentication#list-responses) with saved metrics in `items` (default page size `50`). ### Get, update, delete ```bash theme={null} # Get curl https://your-tell-server/api/v1/metrics/saved/{metric-id} \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" # Update (owner or Admin) curl -X PUT https://your-tell-server/api/v1/metrics/saved/{metric-id} \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{"title": "WAU - Last 90 Days", "query": { "metric": "wau", "range": "90d" }}' # Delete (owner or Admin) curl -X DELETE https://your-tell-server/api/v1/metrics/saved/{metric-id} \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` ## Permissions | Action | Required role | | - | - | | View boards and saved metrics | Viewer | | Create boards and saved metrics | Editor | | Update or delete own content | Editor (owner) | | Update or delete anyone's content | Admin | | Share boards and saved metrics | Editor (owner) or Admin | ## What's next * [Boards & Dashboards](/analytics/boards) — building dashboards as a user * [Sharing](/api/sharing) — creating public share links * [Data Queries](/api/queries) — the metrics that power board blocks # Data Queries Source: https://docs.tell.rs/api/queries Query analytics data — metrics, breakdowns, comparisons, raw drill-down, and SQL. The query API powers Tell's analytics. Use it to fetch active users, events, sessions, logs, and stickiness metrics — with breakdowns, comparisons, and raw drill-down. All endpoints require a JWT token and `X-Workspace-ID` header: ```bash theme={null} curl "https://your-tell-server/api/v1/metrics/dau?range=30d" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` ## Active users Track daily, weekly, and monthly active users: ```bash theme={null} # Daily active users, last 30 days curl "https://your-tell-server/api/v1/metrics/dau?range=30d" # Weekly active users, broken down by country curl "https://your-tell-server/api/v1/metrics/wau?range=90d&breakdown=country" # Monthly active users, compared to previous period curl "https://your-tell-server/api/v1/metrics/mau?range=30d&compare=previous" ``` | Endpoint | Metric | | - | - | | `/metrics/dau` | Daily active users | | `/metrics/wau` | Weekly active users | | `/metrics/mau` | Monthly active users | Each endpoint also has a `/raw` variant (e.g. `/metrics/dau/raw`) that returns the underlying user-level data with pagination. ## Events ```bash theme={null} # Event volume over time curl "https://your-tell-server/api/v1/metrics/events?range=30d&granularity=daily" # Top events by count curl "https://your-tell-server/api/v1/metrics/events/top?range=7d" # Break down an event by a property curl "https://your-tell-server/api/v1/metrics/events/properties?event=purchase&property=plan&range=30d" # Custom aggregation — total revenue curl "https://your-tell-server/api/v1/metrics/events/custom?event=purchase&property=amount&metric=sum&range=30d" ``` | Endpoint | What it returns | | - | - | | `/metrics/events` | Event count over time | | `/metrics/events/top` | Most frequent events ranked by count | | `/metrics/events/top/{name}/raw` | Raw data for a specific event | | `/metrics/events/properties` | Property breakdown (requires `event` and `property` params) | | `/metrics/events/custom` | Aggregation on a property — `sum`, `avg`, `min`, `max`, `count` | | `/metrics/events/raw` | Raw event data | | `/metrics/events/first-time` | First-time event users per period | | `/metrics/events/per-actor` | Per-user event aggregation (avg, p90) | Top lists (`/metrics/events/top`, `/metrics/logs/top`, `/metrics/sessions/top`, and the `top_events` / `top_logs:` metric keys) rank labels, so each entry is a label and its count, with no `date`: ```json theme={null} { "points": [ { "value": 1840.0, "dimension": "page_view" }, { "value": 312.0, "dimension": "signup" } ], "total": 2152.0, "min": 312.0, "max": 1840.0, "avg": 1076.0 } ``` ## First-time events Count users whose first occurrence of an event falls within the query range. See [Events — First-time events](/analytics/events#first-time-events) for usage. ### GET /api/v1/metrics/events/first-time **Parameters:** | Parameter | Type | Required | Description | | - | - | - | - | | `event` | string | Yes | Event name to analyze. | | `match_mode` | boolean | No | Default: `false`. When `true`, filter conditions apply to the historical scan (first-match mode). | | `lookback_days` | integer | No | Limit historical scan to this many days. Omit for full history. | | `range` | string | No | Time range. Default: `30d`. | | `granularity` | string | No | Time bucketing. Default: `daily`. | | `breakdown` | string | No | Break down by a field (e.g., `country`, `properties.plan`). | | `compare` | string | No | `previous` or `yoy`. | **Example:** ```bash theme={null} curl "https://your-tell-server/api/v1/metrics/events/first-time?\ event=sign_up&range=30d&granularity=weekly" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` **Response:** ```json theme={null} { "points": [ {"date": "2025-02-03", "value": 42}, {"date": "2025-02-10", "value": 38}, {"date": "2025-02-17", "value": 55} ], "total": 135, "min": 38, "max": 55, "avg": 45 } ``` With `breakdown=country`, each point includes a `dimension` field: ```json theme={null} { "points": [ {"date": "2025-02-03", "value": 28, "dimension": "US"}, {"date": "2025-02-03", "value": 14, "dimension": "GB"} ] } ``` ## Per-actor metrics Two-level aggregation: first per user, then across users. See [Events — Per-actor metrics](/analytics/events#per-actor-metrics) for usage. ### GET /api/v1/metrics/events/per-actor **Parameters:** | Parameter | Type | Required | Description | | - | - | - | - | | `event` | string | Yes | Event name to analyze. | | `metric` | string | No | Outer aggregation function. Default: `avg`. Options: `avg`, `median`, `p50`, `p75`, `p90`, `p95`, `p99`, `min`, `max`, `sum`. | | `property` | string | No | Property to aggregate per user (e.g., `properties.amount`). If omitted, counts events per user. | | `range` | string | No | Time range. Default: `30d`. | | `granularity` | string | No | Time bucketing. Default: `daily`. | | `breakdown` | string | No | Break down by a field (e.g., `country`, `properties.plan`). | **Example — average events per user:** ```bash theme={null} curl "https://your-tell-server/api/v1/metrics/events/per-actor?\ event=page_view&metric=avg&range=30d&granularity=weekly" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` **Response:** ```json theme={null} { "points": [ {"date": "2025-02-03", "value": 4.5}, {"date": "2025-02-10", "value": 5.2}, {"date": "2025-02-17", "value": 4.8} ], "total": 14.5, "min": 4.5, "max": 5.2, "avg": 4.83 } ``` **Example — p90 revenue per user:** ```bash theme={null} curl "https://your-tell-server/api/v1/metrics/events/per-actor?\ event=purchase&metric=p90&property=properties.amount&range=30d" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` ## Sessions ```bash theme={null} # Session volume curl "https://your-tell-server/api/v1/metrics/sessions?range=30d" # Session volume per day, broken down by device type curl "https://your-tell-server/api/v1/metrics/sessions?range=30d&granularity=daily&breakdown=device_type" # Top sessions by device type curl "https://your-tell-server/api/v1/metrics/sessions/top?range=30d&property=device_type" # Unique sessions curl "https://your-tell-server/api/v1/metrics/sessions/unique?range=30d" ``` ## Logs ```bash theme={null} # Log volume over time curl "https://your-tell-server/api/v1/metrics/logs?range=7d" # Top logs by level curl "https://your-tell-server/api/v1/metrics/logs/top?range=7d" # Filter by service and level curl "https://your-tell-server/api/v1/metrics/logs/raw?service=api&level=error&range=7d" ``` Log-specific filters: `service`, `level`, `source`. `source` is the host or instance that sent the log: the client's configured source, usually the host name. The Rust SDK still lets a per-log `component` replace it, for compatibility only; put a module or tracing target in a log property instead, so `source` stays one value per host. Filter on a field inside a JSON log body with `message.`: ```bash theme={null} curl -X POST "https://your-tell-server/api/v1/metrics/query" \ -H "Content-Type: application/json" \ -d '{"metric": "log_volume", "range": "7d", "filters": [{"field": "message.EventId", "operator": "in", "value": ["4624", "4625"]}]}' ``` The path reads the body's string value. A number in the body does not match a quoted value: `"EventId": "4624"` matches `4624`, `"EventId": 4624` does not. ## Infrastructure metrics Query a metric the SDKs or agents send with the key `metric::`: ```bash theme={null} curl -X POST "https://your-tell-server/api/v1/metrics/query" \ -H "Content-Type: application/json" \ -d '{"metric": "metric:http.latency:hp95", "range": "24h", "granularity": "hourly", "breakdown": "labels.route"}' ``` For gauges and counters, `` aggregates the point value: `avg` (default), `sum`, `min`, `max`, `rate`, `last`, `count`, `median`, `p50`, `p75`, `p90`, `p95`, `p99`. A histogram point has no useful value; its data is the observation count, sum, and buckets. Use the histogram functions for it: | `` | Per time bucket | | - | - | | `hmean` | Mean observation: observed sum / observed count. | | `hp50`, `hp75`, `hp90`, `hp95`, `hp99` | Quantile from the bucket counts, interpolated linearly within the bucket that holds it, as Prometheus `histogram_quantile` does. A quantile that falls in the open-ended top bucket reports the highest finite bound. | Cumulative histograms (the SDK default) count what was observed between consecutive points of each series (one source, service, and label set). A drop in the count is a restart: that point counts whole. The first point of each series in the range is its starting reading and adds nothing. Delta histograms count every point. A bucket with no observations has no point. Break down by `labels.`, `service`, or `source`. ## Users and stickiness ```bash theme={null} # User count over time curl "https://your-tell-server/api/v1/metrics/users?range=30d" # Daily stickiness (DAU/MAU ratio) curl "https://your-tell-server/api/v1/metrics/stickiness/daily?range=30d" # Weekly stickiness (WAU/MAU ratio) curl "https://your-tell-server/api/v1/metrics/stickiness/weekly?range=30d" ``` ## Lifecycle Classify active users into lifecycle states over time. See [Events — Lifecycle](/analytics/events#lifecycle) for usage. ### GET /api/v1/metrics/lifecycle Returns a time series with each active user classified as `new`, `returning`, or `resurrected` per period. **Parameters:** Standard [common parameters](#common-parameters). Default granularity is `weekly`. **Example:** ```bash theme={null} curl "https://your-tell-server/api/v1/metrics/lifecycle?range=30d&granularity=weekly" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` **Response:** ```json theme={null} { "points": [ {"date": "2025-02-03", "value": 150, "dimension": "new"}, {"date": "2025-02-03", "value": 800, "dimension": "returning"}, {"date": "2025-02-03", "value": 50, "dimension": "resurrected"} ], "total": 1000, "min": 50, "max": 800, "avg": 333 } ``` ## Segments Snapshot classification of all users into lifecycle and engagement segments. See [Segments](/analytics/segments) for definitions. ### GET /api/v1/metrics/segments Returns user counts for all 8 built-in segments. **Parameters:** | Parameter | Type | Required | Description | | - | - | - | - | | `range` | string | No | Time range for computation. Default: `30d`. | **Example:** ```bash theme={null} curl "https://your-tell-server/api/v1/metrics/segments?range=30d" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` **Response:** ```json theme={null} [ {"segment": "new", "category": "lifecycle", "users": 342}, {"segment": "returning", "category": "lifecycle", "users": 1205}, {"segment": "dormant", "category": "lifecycle", "users": 587}, {"segment": "churned", "category": "lifecycle", "users": 2341}, {"segment": "one_timer", "category": "engagement", "users": 891}, {"segment": "casual", "category": "engagement", "users": 645}, {"segment": "regular", "category": "engagement", "users": 412}, {"segment": "power_user", "category": "engagement", "users": 189} ] ``` ### GET /api/v1/metrics/segments/:segment Drill into a specific segment to see individual users. **Parameters:** | Parameter | Type | Required | Description | | - | - | - | - | | `range` | string | No | Time range. Default: `30d`. | | `limit` | integer | No | Max users returned. Default: `50`, max: `100`. | **Example:** ```bash theme={null} curl "https://your-tell-server/api/v1/metrics/segments/dormant?range=90d&limit=20" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` **Response (lifecycle segment):** ```json theme={null} [ { "device_id": "a1b2c3d4", "segment": "dormant", "first_seen": "2025-01-10T08:30:00Z", "last_seen": "2025-02-05T14:22:00Z" } ] ``` **Response (engagement segment):** ```json theme={null} [ { "device_id": "a1b2c3d4", "segment": "power_user", "session_count": 47 } ] ``` ## Formulas Combine multiple metric series with a math expression. See [Events — Formulas](/analytics/events#formulas) for syntax and usage. ### POST /api/v1/metrics/formula Evaluate a formula across multiple metric series. **Request body:** | Field | Type | Required | Description | | - | - | - | - | | `series` | array | Yes | Metric series to evaluate. Each has `metric` (string), optional `filters` (array), optional `breakdown` (string), optional `range` (string override). | | `formula` | string | Yes | Math expression using series references A–Z. | | `range` | string | No | Time range. Default: `30d`. | | `granularity` | string | No | Time bucketing. Default: `daily`. | **Series metric values:** `dau`, `wau`, `mau`, `events`, `event:`, `sessions`, `users`, `logs`. **Example:** ```bash theme={null} curl -X POST https://your-tell-server/api/v1/metrics/formula \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{ "series": [ { "metric": "event:sign_up" }, { "metric": "event:purchase" } ], "formula": "B / A * 100", "range": "30d", "granularity": "daily" }' ``` **Response:** ```json theme={null} { "points": [ { "date": "2025-06-01", "value": 16.2 }, { "date": "2025-06-02", "value": 18.5 } ], "total": 17.1, "min": 12.0, "max": 22.4, "avg": 17.1 } ``` ## Grouped metrics Proportional composition with two metrics per group. See [Events — Grouped metrics](/analytics/events#grouped-metrics) for usage. ### POST /api/v1/metrics/grouped Compute grouped metrics for treemap visualization. **Request body:** | Field | Type | Required | Description | | - | - | - | - | | `group_by` | string | Yes | Field to group by (e.g., `country`, `event_name`, `properties.plan`). | | `size` | object | Yes | Size metric: `{ "type": "count" }`, `{ "type": "unique_actors" }`, or `{ "type": "sum", "property": "properties.revenue" }`. | | `intensity` | object | Yes | Intensity metric: `{ "type": "avg_per_actor" }`, `{ "type": "percentile_per_actor", "percentile": 0.9 }`, `{ "type": "unique_actors" }`, or `{ "type": "avg", "property": "properties.load_time" }`. | | `event` | string | No | Filter to a specific event. | | `range` | string | No | Time range. Default: `30d`. | | `max_groups` | integer | No | Max groups returned. Default: `20`. | **Example:** ```bash theme={null} curl -X POST https://your-tell-server/api/v1/metrics/grouped \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{ "group_by": "country", "size": { "type": "unique_actors" }, "intensity": { "type": "avg_per_actor" }, "range": "30d" }' ``` **Response:** ```json theme={null} { "groups": [ { "group": "US", "size": 1500, "intensity": 3.2, "children": [] }, { "group": "CA", "size": 800, "intensity": 2.8, "children": [] }, { "group": "UK", "size": 200, "intensity": 2.5, "children": [] } ], "total_size": 2500, "size_label": "User Count", "intensity_label": "Avg Events/User", "intensity_min": 2.5, "intensity_max": 3.2 } ``` ## Funnels Analyze step-by-step conversion. See the [Funnels product guide](/analytics/funnels) for concepts and examples. ### POST /api/v1/funnels Run a funnel analysis across a sequence of events. **Request body:** | Field | Type | Required | Description | | - | - | - | - | | `steps` | array | Yes | 2–10 funnel steps. Each step has `event` (string) and optional `filters` (array of conditions). | | `window_seconds` | integer | No | Max seconds for a user to complete all steps. Default: `604800` (7 days). | | `breakdown` | string | No | Split results by a dimension (e.g., `country`, `device_type`). | | `range` | string | No | Time range. Default: `30d`. | **Step filter fields:** | Field | Type | Required | Description | | - | - | - | - | | `field` | string | Yes | Property to filter on (e.g., `properties.plan`). | | `operator` | string | No | Comparison operator. Default: `eq`. | | `value` | any | No | Value to compare against. String, number, or array (for `in`/`not_in`). | **Example:** ```bash theme={null} curl -X POST https://your-tell-server/api/v1/funnels \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{ "steps": [ { "event": "sign_up" }, { "event": "purchase" } ], "window_seconds": 604800, "range": "30d" }' ``` **Response:** ```json theme={null} { "steps": [ { "step": 1, "event": "sign_up", "count": 1200, "overall_rate": 1.0, "step_rate": 1.0, "drop_off": 0 }, { "step": 2, "event": "purchase", "count": 192, "overall_rate": 0.16, "step_rate": 0.16, "drop_off": 1008 } ], "overall_conversion": 0.16 } ``` ### POST /api/v1/funnels/trend Track funnel conversion rate over time. Same request body as `POST /api/v1/funnels`, plus a `granularity` field. | Field | Type | Required | Description | | - | - | - | - | | `granularity` | string | No | Time bucket size: `minute`, `hourly`, `daily`, `weekly`, `monthly`. Default: `daily`. | **Example:** ```bash theme={null} curl -X POST https://your-tell-server/api/v1/funnels/trend \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{ "steps": [ { "event": "sign_up" }, { "event": "purchase" } ], "window_seconds": 604800, "range": "30d", "granularity": "daily" }' ``` Returns time series data with conversion rate per time bucket. ## Retention Cohort retention analysis. See the [Retention product guide](/analytics/retention) for how to read the cohort matrix. ### POST /api/v1/retention Run a cohort retention analysis. **Request body:** | Field | Type | Required | Description | | - | - | - | - | | `granularity` | string | No | Cohort bucketing: `daily`, `weekly`, `monthly`. Default: `weekly`. | | `periods` | integer | No | Number of periods to track (1–52). Default: `8`. | | `start_event` | string | No | Cohort entry event. Default: any event. | | `return_event` | string | No | Return activity event. Default: any event. | | `range` | string | No | Time range. Default: `90d`. | **Example:** ```bash theme={null} curl -X POST https://your-tell-server/api/v1/retention \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{ "granularity": "weekly", "periods": 8, "start_event": "sign_up", "range": "90d" }' ``` **Response:** ```json theme={null} { "cohorts": [ { "cohort_date": "2025-04-07", "cohort_size": 320, "retention": [1.0, 0.45, 0.32, 0.28], "counts": [320, 144, 102, 90] } ], "averages": [1.0, 0.45, 0.32, 0.28], "granularity": "week", "periods": 8 } ``` ## Audiences Manage saved user groups. See the [Audiences product guide](/analytics/audiences) for definition syntax and usage. ### GET /api/v1/audiences List all audiences in the workspace. Requires Viewer role or above. ```bash theme={null} curl https://your-tell-server/api/v1/audiences \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` **Response:** ```json theme={null} { "items": [ { "id": "aud_x7k9m2", "name": "US power buyers", "description": "US users who purchased in the last 30 days", "definition": { "filters": [...], "events": [...] }, "created_at": "2025-06-15T10:30:00Z", "updated_at": "2025-06-15T10:30:00Z" } ], "total": 1, "limit": 50, "offset": 0, "has_more": false } ``` This is a [list response](/api/authentication#list-responses). Pass `limit` and `offset` to page. ### POST /api/v1/audiences Create an audience. Requires Editor role or above. **Request body:** | Field | Type | Required | Description | | - | - | - | - | | `name` | string | Yes | Audience name (1–200 characters). | | `description` | string | No | Description (max 2000 characters). | | `definition` | object | Yes | Audience rules. Contains `filters` (array), `events` (array), and optional `lookback_days` (integer). | **Example:** ```bash theme={null} curl -X POST https://your-tell-server/api/v1/audiences \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{ "name": "US power buyers", "definition": { "filters": [ { "field": "country", "operator": "eq", "value": "US" } ], "events": [ { "event": "purchase", "days": 30 } ] } }' ``` ### GET /api/v1/audiences/:id Get a single audience by ID. ### PUT /api/v1/audiences/:id Update an audience. Only the owner or a workspace admin can update. All fields are optional — omit a field to keep its current value. **Request body:** | Field | Type | Required | Description | | - | - | - | - | | `name` | string | No | New name. | | `description` | string | No | New description. | | `definition` | object | No | Updated audience rules. | ### DELETE /api/v1/audiences/:id Delete an audience. Only the owner or a workspace admin can delete. ### GET /api/v1/audiences/:id/count Get the number of users matching an audience's rules. **Response:** ```json theme={null} { "audience_id": "aud_x7k9m2", "count": 3842 } ``` ## Marks Timeline annotations that overlay on charts. See [Boards — Marks](/analytics/boards#marks) for product usage. ### GET /api/v1/marks List marks in the workspace. Public marks are visible to all members. Private marks are only visible to their creator. **Query parameters:** | Parameter | Type | Required | Description | | - | - | - | - | | `from` | string | No | Start time (ISO 8601). | | `to` | string | No | End time (ISO 8601). | | `category` | string | No | Filter by category: `release`, `news`, `incident`. | | `source` | string | No | Filter by source: `manual`, `api`, `plugin`. | | `limit` | integer | No | Page size. Default: `50`, max: `200`. | | `offset` | integer | No | Rows to skip. Default: `0`. | **Example:** ```bash theme={null} curl "https://your-tell-server/api/v1/marks?category=release&from=2025-06-01T00:00:00Z" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` **Response:** ```json theme={null} { "items": [ { "id": "mrk_a1b2c3", "title": "Shipped v2.1.0", "description": null, "timestamp": "2025-06-15T14:30:00Z", "category": "release", "source": "manual", "source_ref": "https://github.com/example/repo/releases/tag/v2.1.0", "scope": "public", "created_by": "usr_abc", "created_at": "2025-06-15T14:30:00Z", "updated_at": "2025-06-15T14:30:00Z" } ], "total": 1, "limit": 50, "offset": 0, "has_more": false } ``` This is a [list response](/api/authentication#list-responses). ### GET /api/v1/marks/:id Get a single mark by ID. Returns 404 for private marks belonging to other users. ### POST /api/v1/marks Create a mark. Requires Editor role or above. **Request body:** | Field | Type | Required | Description | | - | - | - | - | | `title` | string | Yes | Mark title (1–200 characters). | | `description` | string | No | Description (max 2000 characters). | | `timestamp` | string | No | When the event happened (ISO 8601). Default: now. | | `category` | string | No | `release`, `news`, or `incident`. Default: `release`. | | `source_ref` | string | No | URL, commit SHA, or other reference. | | `scope` | string | No | `public` or `private`. Default: `public`. | | `metadata` | string | No | JSON blob (max 10KB). | ### DELETE /api/v1/marks/:id Delete a mark. Only the creator or a workspace admin can delete. ## Common parameters Most metric endpoints share these query parameters: | Parameter | Default | Options | | - | - | - | | `range` | `30d` | `7d`, `30d`, `90d`, or custom: `2025-01-01,2025-01-31` | | `granularity` | `daily` | `minute`, `hourly`, `daily`, `weekly`, `monthly` | | `breakdown` | — | Any dimension: `country`, `platform`, `device_type`, `os`, etc. | | `compare` | — | `previous` (previous period) or `yoy` (year-over-year) | | `conditions[N][field]` | — | Filter conditions (see [Filtering](/analytics/filtering)) | Raw endpoints add pagination: | Parameter | Default | Description | | - | - | - | | `limit` | `100` | Max rows returned (up to 10,000) | | `offset` | `0` | Rows to skip, newest first | | `count_total` | `false` | Also return `total`, the match count across all pages (slower) | A raw endpoint returns one page of rows: ```json theme={null} { "items": [ { "timestamp": "2025-01-31 12:00:00.000", "event_name": "purchase", "device_id": "…" } ], "limit": 100, "offset": 0, "has_more": true, "total": 4821 } ``` `has_more` says whether another page follows; request it with `offset` set to `offset + limit`. `total` is present only with `count_total=true`. ## SQL queries For full flexibility, execute SQL directly against your analytics data. Rate-limited to **30 requests per minute**. ```bash theme={null} curl -X POST https://your-tell-server/api/v1/data/query \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{"query": "SELECT event, count() as cnt FROM events_v1 GROUP BY event ORDER BY cnt DESC LIMIT 10"}' ``` Returns: ```json theme={null} { "columns": [ { "name": "event", "data_type": "String" }, { "name": "cnt", "data_type": "UInt64" } ], "rows": [ ["page_view", 48210], ["click", 12503] ], "row_count": 2, "execution_time_ms": 42 } ``` Only `SELECT` statements are allowed. Queries are automatically scoped to your workspace database. Max 10,000 rows per query (default 1,000). ## Data exploration Discover what data is available before writing queries: ```bash theme={null} # List available data sources (a list response with sources in `items`) curl https://your-tell-server/api/v1/data/sources \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" # Get fields for a source curl https://your-tell-server/api/v1/data/sources/events/fields \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" # Get distinct values for a field curl "https://your-tell-server/api/v1/data/sources/events/values/country?limit=20" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` ### Available sources | Source | Table | Description | | - | - | - | | `events` | events\_v1 | Custom events and page views | | `logs` | logs\_v1 | Structured logs | | `context` | context\_v1 | Device and session context | | `user_traits` | user\_traits\_v1 | User properties set via `identify` | | `users` | users\_v1 | Unique users | | `user_devices` | user\_devices | Device-to-user mappings | ## What's next * [Boards & Metrics](/api/boards) — save and visualize query results * [Sharing](/api/sharing) — share metrics and boards publicly * [SQL Analytics](/analytics/sql) — SQL query patterns and tips # Sharing Source: https://docs.tell.rs/api/sharing Public share links for boards and saved metrics. Share boards and saved metrics with anyone via a unique URL — no login required. Share links are useful for embedding dashboards in wikis, sending reports to stakeholders, or making specific metrics publicly accessible. ## Create a share link ### For a board ```bash theme={null} curl -X POST https://your-tell-server/api/v1/boards/{board-id}/share \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` ### For a saved metric ```bash theme={null} curl -X POST https://your-tell-server/api/v1/metrics/saved/{metric-id}/share \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` Both return: ```json theme={null} { "hash": "a1b2c3d4e5f6", "url": "https://your-tell-server/s/b/a1b2c3d4e5f6", "expires_at": null, "created_at": "2025-06-15T10:00:00Z" } ``` Only the board/metric owner or an Admin can create share links. Each resource gets one share link at a time — creating a new one replaces the old one. ## View a shared resource Anyone with the link can view the shared content without logging in: ```bash theme={null} # Shared board curl https://your-tell-server/s/b/{hash} # Shared metric curl https://your-tell-server/s/m/{hash} ``` Shared views return the content (title, description, settings/query) without any owner or workspace information. ## Get the current share link ```bash theme={null} # For a board curl https://your-tell-server/api/v1/boards/{board-id}/share \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" # For a saved metric curl https://your-tell-server/api/v1/metrics/saved/{metric-id}/share \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` Returns `404` if no share link exists. ## Revoke a share link ```bash theme={null} # For a board curl -X DELETE https://your-tell-server/api/v1/boards/{board-id}/share \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" # For a saved metric curl -X DELETE https://your-tell-server/api/v1/metrics/saved/{metric-id}/share \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` Once revoked, the public URL immediately stops working. ## Security * Share URLs use a random hash — they can't be guessed or enumerated * Public share endpoints are rate-limited to **60 requests per minute** to prevent brute-force scanning * Shared views strip owner and workspace information from the response * Expired links return `404` * All share creation and revocation events are [audit logged](/running-tell/audit-privacy) ## Permissions | Action | Required role | | - | - | | Create share link | Owner (Editor+) or Admin | | View share link status | Owner or Admin | | Revoke share link | Owner or Admin | | View shared content | Anyone with the URL | ## What's next * [Boards & Metrics](/api/boards) — managing the resources you're sharing * [Audit & Privacy](/running-tell/audit-privacy) — tracking who creates and revokes share links # Workspaces Source: https://docs.tell.rs/api/workspaces Create, list, and manage workspaces via the API. Workspaces isolate your data — each workspace gets its own database, API keys, and team members. Use these endpoints to create and manage workspaces programmatically. All workspace endpoints require a JWT token in the `Authorization` header. See [Authentication](/api/authentication) for details. ## Create a workspace Any authenticated user with Editor or higher role can create a workspace. You become the admin of the new workspace automatically. ```bash theme={null} curl -X POST https://your-tell-server/api/v1/user/workspaces \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "Production", "slug": "production"}' ``` The `slug` must be 1-50 characters, alphanumeric and hyphens only, and unique across all workspaces. Returns: ```json theme={null} { "id": "ws_abc123", "name": "Production", "slug": "production", "created_at": "2025-03-15T10:00:00Z", "updated_at": "2025-03-15T10:00:00Z" } ``` ## List your workspaces ```bash theme={null} curl https://your-tell-server/api/v1/user/workspaces \ -H "Authorization: Bearer $TOKEN" ``` Returns the workspaces you're a member of as a [list response](/api/authentication#list-responses): ```json theme={null} { "items": [ { "id": "ws_abc123", "name": "Production", "slug": "production" } ], "total": 1, "limit": 200, "offset": 0, "has_more": false } ``` The default page size is `200`. ## Admin endpoints Workspace admins and platform users have access to additional management endpoints. These require the `X-Workspace-ID` header. ### List all workspaces (Platform only) ```bash theme={null} curl https://your-tell-server/api/v1/admin/workspaces \ -H "Authorization: Bearer $TOKEN" ``` Returns a [list response](/api/authentication#list-responses) with workspaces in `items`. The default page size is `200`. ### Get workspace details ```bash theme={null} curl https://your-tell-server/api/v1/admin/workspaces/{id} \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` ### Update workspace name ```bash theme={null} curl -X PUT https://your-tell-server/api/v1/admin/workspaces/{id} \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{"name": "Production Analytics"}' ``` ### Delete workspace (Platform only) ```bash theme={null} curl -X DELETE https://your-tell-server/api/v1/admin/workspaces/{id} \ -H "Authorization: Bearer $TOKEN" ``` This permanently deletes the workspace and all its data. Platform role required. ## Invitations Admins can invite users to a workspace by email. Invitations are assigned a role and expire after 7 days. ### Invite a user ```bash theme={null} curl -X POST https://your-tell-server/api/v1/admin/workspace/invites \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"email": "teammate@example.com", "workspace_id": "ws_abc123", "role": "editor"}' ``` The response includes an `invite_url` you can share with the invited user. ### List pending invites ```bash theme={null} curl "https://your-tell-server/api/v1/admin/workspace/invites?workspace_id=ws_abc123&status=pending" \ -H "Authorization: Bearer $TOKEN" ``` Returns a [list response](/api/authentication#list-responses) with invites in `items`. ### Cancel an invite ```bash theme={null} curl -X DELETE "https://your-tell-server/api/v1/admin/workspace/invites/{invite-id}?workspace_id=ws_abc123" \ -H "Authorization: Bearer $TOKEN" ``` ### Accept an invite (no auth required) Invited users visit the invite URL to verify and accept: ```bash theme={null} # Verify the invite curl https://your-tell-server/api/v1/invites/{token} # Accept it curl -X POST https://your-tell-server/api/v1/invites/{token}/accept \ -H "Content-Type: application/json" \ -d '{"email": "teammate@example.com", "password": "their-password"}' ``` New users are created automatically when accepting an invite. ## User management Admins can manage users in their workspace. You can't assign a role higher than your own. ```bash theme={null} # List users curl "https://your-tell-server/api/v1/admin/users?limit=50" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" # Create a user curl -X POST https://your-tell-server/api/v1/admin/users \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{"email": "dev@example.com", "password": "secure-password", "role": "editor"}' # Update a user's role curl -X PUT https://your-tell-server/api/v1/admin/users/{user-id} \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{"role": "admin"}' # Delete a user curl -X DELETE https://your-tell-server/api/v1/admin/users/{user-id} \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` ### User list filters | Parameter | Description | | - | - | | `email` | Filter by email (partial match) | | `role` | Filter by role | | `limit` | Page size (default 50, max 200) | | `offset` | Rows to skip | The response is a [list response](/api/authentication#list-responses) with users in `items` and the number of matching users in `total`. ## Error responses All endpoints return consistent error JSON: ```json theme={null} { "error": "VALIDATION_ERROR", "message": "Slug already taken" } ``` | Code | Status | Meaning | | - | - | - | | `UNAUTHORIZED` | 401 | Missing or invalid token | | `FORBIDDEN` | 403 | Insufficient role | | `NOT_FOUND` | 404 | Workspace doesn't exist | | `CONFLICT` | 409 | Slug already taken | | `VALIDATION_ERROR` | 422 | Invalid input | ## What's next * [Workspaces & Teams](/running-tell/workspaces) — operational guide for workspace management * [Roles & Permissions](/running-tell/roles) — what each role can do * [Authentication](/api/authentication) — tokens and login flow # Architecture Source: https://docs.tell.rs/getting-started/architecture How Tell deploys — from a single binary to distributed edge-to-cloud topologies. Tell is a single Rust binary. How you deploy it depends on your scale, your environment, and where your data needs to go. Here are the four topologies. ## Single node Everything on one machine — ingestion, pipeline, storage, and query. ``` SDKs / Sources ─── [ Tell ] ─── ClickHouse / Disk ``` This is the default. Install Tell, start it, point your SDKs at it. One binary handles TCP and HTTP ingestion, runs the pipeline, writes to ClickHouse or local disk, and serves the API and dashboards. A single node handles up to 64M events/sec on batched TCP ingestion. Storage write throughput depends on your sink — ClickHouse, Parquet, Arrow IPC, and raw disk all have different profiles. **Good for:** indie projects, startups, small teams, development, and honestly most production workloads under moderate volume. ```bash theme={null} curl -sSfL https://tell.rs | bash tell run -c config.toml ``` Read more: [Self-hosting](/running-tell/self-hosting) | [Pipeline Configuration](/pipeline/configuration) ## Distributed Edge nodes pre-process data and forward to a central collector. Each edge node runs Tell with transforms — filtering, redacting PII, extracting log patterns — then forwards pre-formatted batches to an upstream Tell instance via the forwarder sink. ``` ┌─── [ Tell edge + transforms ] ───┐ Sources ────┤ ├─── [ Tell collector ] ─── ClickHouse └─── [ Tell edge + transforms ] ───┘ ``` The forwarding path uses FlatBuffers with batched zero-copy ingestion — the performance-optimized path. Edge nodes handle the heavy per-event work (parsing, redaction, pattern extraction) so the central collector just routes and writes. Each edge node handles 100K–1M+ events/sec depending on transforms configured. The central collector handles raw forwarded batches at full pipeline speed. **Good for:** high-volume production, multi-region deployments, enterprises, anywhere you need to pre-process close to the source. Read more: [Forwarder sink](/pipeline/sinks/forwarder) | [Transforms](/pipeline/transforms/overview) | [Routing](/pipeline/routing) ## Collect and forward Collect data locally — write to disk — then forward to an upstream system. The upstream can be another Tell instance, a SOC, an MSSP, or any system that accepts the forwarded format. ``` Sources ─── [ Tell ] ─── Disk (Parquet / Arrow / binary) └─── Forwarder ─── SOC / MSSP / Tell Cloud ``` This topology is designed for environments where data needs to be captured reliably at the edge, even when the upstream connection is intermittent or unavailable. Local disk storage acts as a buffer and an audit trail. **Good for:** OT and industrial environments, air-gapped networks, compliance-heavy deployments, managed security providers. Read more: [Disk sink](/pipeline/sinks/disk) | [Parquet sink](/pipeline/sinks/parquet) | [Forwarder sink](/pipeline/sinks/forwarder) ## Tell Cloud Point your SDKs directly at Tell Cloud. No infrastructure to manage. ``` SDKs ─── tell.rs ─── managed storage + dashboards ``` **Coming soon.** Same SDKs, same data model, same query API — hosted by Tell. ## Mixing topologies These aren't mutually exclusive. You can run a single node for your web app's product analytics, a distributed setup for high-volume backend logs, and a collect-and-forward node in an air-gapped factory — all feeding into the same Tell Cloud or self-hosted collector. ## What's next * [Quickstart](/getting-started/quickstart) — get a single node running in under 5 minutes * [Self-hosting](/running-tell/self-hosting) — production deployment guide * [Pipeline Overview](/pipeline/overview) — sources, transforms, sinks, and routing # Concepts Source: https://docs.tell.rs/getting-started/concepts Tell's three data protocols — events, logs, and snapshots — and how they work together. Tell has three data protocols. Each handles a different kind of signal, but they all flow through the same server and can be queried together. ## Events Events are your product analytics — the things users do in your app. Page views, signups, purchases, feature toggles. If you've used Mixpanel, Segment, or PostHog, this works the same way. ```javascript theme={null} tell.track('purchase', { plan: 'pro', amount: 49.99 }); ``` Every event has a **name** and optional **properties** — key-value pairs you can use for breakdowns, filters, and aggregations. "Show me purchases broken down by `plan`" or "daily active users by `country`." Within the events protocol, there are several message types: | Type | What it does | | - | - | | **track** | Record a user action — `page_view`, `sign_up`, `purchase` | | **identify** | Link a device to a known user ID when they log in | | **session** | Device and session context (SDKs send this automatically) | | **group** | Associate a user with a company or team | | **revenue** | Track a purchase with amount, currency, and order ID | | **alias** | Merge two user identities across devices | Most of the time you'll use `track` and `identify`. Sessions are managed automatically by the SDK — when a user opens your app, a session starts and groups all their events together. The rest are for specific use cases. Read more: [Events & Properties](/tracking/events-and-properties) | [Users & Identity](/tracking/users-and-identity) | [Sessions](/tracking/sessions) ## Logs Logs are your application's internal voice — errors, warnings, debug info. Tell is a full structured logging framework with 9 severity levels, service tags, and arbitrary metadata. ```javascript theme={null} tell.log('error', 'Payment failed', { service: 'billing', error_code: 'CARD_DECLINED', user_id: 'user-123' }); ``` The same SDK that sends your product analytics also sends your logs. They're stored separately, but because they share a pipeline and a server, you can correlate them. A user reports a bug — you see their event trail *and* the errors your app threw during that session. Read more: [Logs](/tracking/logs) ## Snapshots Snapshots pull data from services outside your app on a schedule. You connect a service like GitHub or Shopify, and Tell pulls metrics over time automatically. ```toml theme={null} [[plugins]] name = "github" entity = "your-org/your-repo" config.token = "${GITHUB_TOKEN}" ``` This captures stars, forks, issues, and commits on a schedule — a time series of your repository's growth without writing any code. Shopify works the same way for orders, revenue, and customers. Snapshots are how you get the full business picture alongside your product analytics. Track what users do *and* how the business responds. Read more: [Integrations](/tracking/integrations/overview) ## Properties Properties are the key-value pairs attached to events and logs. They're what make your data queryable. * **Event properties**: `plan`, `amount`, `source`, `page_url` * **User traits**: `email`, `company`, `signup_date` (set via `identify`) * **Log fields**: `service`, `level`, `error_code` Any metric can be broken down by any property. Any property can be filtered with 15 operators — equals, contains, greater than, regex, and more. ## The pipeline When data arrives — from an SDK, an HTTP request, or an integration — it passes through a pipeline before being stored. The pipeline routes data to the right destination, and optionally filters, redacts PII, or transforms it in flight. For most setups you don't need to configure this. The defaults work. When you need more control — routing logs to disk and events to ClickHouse, redacting emails before storage, filtering out health checks — the pipeline is fully configurable. Read more: [Pipeline Overview](/pipeline/overview) ## What's next * [Architecture](/getting-started/architecture) — deployment topologies from single node to distributed * [Quickstart](/getting-started/quickstart) — send your first event in under 5 minutes * [What to Track](/getting-started/what-to-track) — choosing between events, logs, and snapshots # Quickstart Source: https://docs.tell.rs/getting-started/quickstart Install Tell, start the server, and see your data flowing in under a minute. ## Get Running ```bash theme={null} curl -sSfL https://tell.rs | bash ``` This installs a single binary with everything included — server, CLI, TUI, and MCP server. Requires Rust 1.80+ and Cargo: ```bash theme={null} git clone https://github.com/tell-rs/tell.git cd tell cargo install --path crates/tell ``` ```bash theme={null} tell init ``` Creates a `tell-config.toml` in your current directory with sensible defaults. ```bash theme={null} tell run ``` Tell is running. That's it. ## See Live Data Open a new terminal and start streaming: ```bash theme={null} tell tail ``` In another terminal, send some test events: ```bash theme={null} tell test ``` Watch the events flow through in real-time. `tell tail` supports filters, sampling, and regex matching — see [Live Tail](/tools/live-tail) for details. ## Connect Your Data ### SDKs Ingest events and logs from your apps: Node.js, browsers, React, Next.js Backend services iOS, macOS Cross-platform mobile Rust services Native applications ### Plugins Pull data from external services: ```bash theme={null} tell plugin install github ``` See what's available: ```bash theme={null} tell plugin list ``` For configuration and all available integrations, see [Integrations](/tracking/integrations/overview). ## Query with AI Add your LLM API key to `tell-config.toml` and restart Tell: ```bash theme={null} tell run ``` Log in and start asking questions: ```bash theme={null} tell login tell ask "what's happening with my signups this week?" ``` Or use the web interface or desktop app for a visual experience. **Tell Cloud** is a managed version of Tell — same SDKs, same data model, same query API. No infrastructure to manage. Coming soon at [tell.rs](https://tell.rs). ## What's next * [What to Track](/getting-started/what-to-track) — choosing between events, logs, and snapshots * [Events & Properties](/tracking/events-and-properties) — the full event model * [Self-hosting](/running-tell/self-hosting) — production deployment with ClickHouse, systemd, and TLS * [Pipeline Configuration](/pipeline/configuration) — customize routing, transforms, and sinks # What is Tell? Source: https://docs.tell.rs/getting-started/what-is-tell Product analytics, structured logs, and business signals — unified. Track what users do, what your app does, and what your business does. Query them together. Tell is a unified analytics platform for product events, structured logs, and business signals. Track what users do, what your app does, and what your business does — then query them together. That's the point. Most analytics tools show you *what* happened. Tell shows you *why*, because your product metrics, application logs, and external business data live in the same place. When signups drop, you don't switch between Mixpanel, Datadog, and a spreadsheet — you see the broken API call, the user journey, and the Shopify revenue impact in one query. ## What you can do **Track user behavior.** Page views, signups, purchases, feature usage, funnels, retention — the things product and growth teams need. If you've used Mixpanel or PostHog, this is familiar. SDKs for JavaScript, Go, Swift, Flutter, Rust, and C++. **Ingest structured logs.** Errors, warnings, debug info — with severity levels, service tags, and metadata. Not a sidecar. A full logging framework that shares the same SDK as your analytics, so you can correlate what users were doing when something broke. **Connect business signals.** Pull metrics from GitHub, Shopify, and other services on a schedule. Track repository stars, store revenue, customer counts — anything that lives outside your app but matters to your business. **Query everything.** SQL queries against ClickHouse. [Boards](/analytics/boards) with markdown and public sharing. CLI with live tail and instant queries. AI tools that answer questions about your data. ## How it works You add a Tell SDK to your app. It sends product events and logs to a Tell server. Integrations pull in external data on a schedule. Everything flows through a pipeline that can route, filter, redact PII, and transform data before it lands in storage. One SDK. One server. One binary. ```javascript theme={null} // Product analytics — what users do tell.track('purchase', { plan: 'pro', amount: 49.99 }); // Structured logs — what your app does tell.log('warn', 'Payment retry failed', { service: 'billing', error: 'GATEWAY_TIMEOUT', attempt: 3, user_id: 'user-123' }); ``` Both calls go through the same SDK to the same server. Stored separately, queryable together. ## Why Tell * **Stupid fast.** 64M events/sec on batched TCP. Rust pipeline with sub-millisecond latency. * **One binary.** No containers. No Docker Compose. Runs on a VPS or your enterprise cluster. * **Self-hosted.** Your data never leaves your servers. No cloud dependency. Tell Cloud is also available if you prefer managed. * **Connect everything.** Product events, structured logs, business data. Query them together. ## Who it's for Tell fits anywhere you have data in multiple places that should be in one. * **Product and growth teams** — funnels, retention, DAU/WAU/MAU, and boards you own. The analytics you'd get from Mixpanel or PostHog, self-hosted and correlated with your logs. * **Engineering teams** — debug production issues with the full picture: user events, application errors, and session context in one query. * **Ecommerce and DTC** — unify checkout funnels, Shopify orders, campaign metrics, and backend logs. Answer "what changed?" when revenue drops. * **Industrial and OT** — collect and forward logs from air-gapped networks. One Rust binary, deterministic performance, no cloud required. * **Small teams** — stop paying for four tools that don't talk to each other. ## What's next * [Concepts](/getting-started/concepts) — the three data protocols and how they work * [Quickstart](/getting-started/quickstart) — send your first event in under 5 minutes * [What to Track](/getting-started/what-to-track) — choosing between events, logs, and snapshots # What to Track Source: https://docs.tell.rs/getting-started/what-to-track When to use events vs logs vs snapshots — and how to name them. Tell has three data protocols. Choosing the right one for each signal keeps your data clean and your queries fast. ## Events, logs, or snapshots? | Signal | Protocol | Example | | - | - | - | | A user does something | **Event** | `sign_up`, `purchase`, `page_view` | | Your app does something | **Log** | Error, warning, deploy, job completion | | An external service has a metric | **Snapshot** | GitHub stars, Shopify revenue, Cloudflare requests | **Events** are user-centric. Every event is tied to a device and a user. Use events for anything you'd put in a product analytics tool — signups, purchases, feature usage, page views, funnels, retention. **Logs** are infrastructure-centric. They're tied to a service and a severity level, not a user. Use logs for errors, warnings, deploys, background job status — anything your application does that isn't a direct user action. **Snapshots** pull metrics from external services on a schedule. Use snapshots for data that lives outside your app — repository stars, store revenue, email delivery rates. ### When it's unclear Some signals could go either way. Here's how to decide: | Signal | Use event or log? | Why | | - | - | - | | User clicks "Export" | **Event** | It's a user action you want in funnels and retention | | Export job fails | **Log** | It's an application error, not a user action | | User sees an error page | **Log** | It's an application error — count and filter by user in log queries | | Server returns 500 | **Log** | It's infrastructure behavior | | Payment succeeds | **Event** (revenue) | It's a business outcome tied to a user | | Payment gateway times out | **Log** | It's a system failure | | API rate limit hit | **Log** | It's infrastructure, even if a user triggered it | The test: **would a product manager care about this in a funnel or retention chart?** If yes, it's an event. If it's something an engineer debugs in a log viewer, it's a log. ## Naming events Use `snake_case`. Be specific. Name the action, not the UI element. ```javascript theme={null} // Good tell.track('sign_up', { plan: 'free' }); tell.track('invite_sent', { role: 'editor' }); tell.track('report_exported', { format: 'csv' }); // Bad tell.track('click_button'); // what button? tell.track('SignUp'); // use snake_case tell.track('user_did_a_thing'); // be specific ``` ### Naming conventions | Pattern | Example | When to use | | - | - | - | | `noun_verb` | `report_exported`, `invite_sent` | Completed actions (most events) | | `noun_verb` | `checkout_started`, `trial_ended` | State transitions | | `page_view` | `page_view` | Page or screen views (use this exact name) | | `feature_used` | `feature_used` with `feature` property | Generic feature tracking | Keep your event namespace flat. Don't use dots or slashes — `purchase` not `ecommerce.purchase`. Use properties for dimensions, not the event name. ```javascript theme={null} // Good — one event name, dimensions in properties tell.track('purchase', { category: 'subscription', plan: 'pro' }); tell.track('purchase', { category: 'one_time', product: 'report' }); // Bad — splitting into multiple event names tell.track('subscription_purchase', { plan: 'pro' }); tell.track('one_time_purchase', { product: 'report' }); ``` The first approach gives you one funnel step, one retention event, and one metric — broken down by `category`. The second fragments your data. ## Choosing properties Properties are the key-value pairs that make your events queryable. Every property you send becomes a breakdown, filter, or aggregation dimension. ### What to include | Property type | Examples | Why | | - | - | - | | **What happened** | `plan`, `amount`, `format`, `source` | Core dimensions for breakdowns | | **Where it happened** | `page_url`, `screen`, `section` | Location context | | **Experiment context** | `variant`, `experiment_id` | A/B test analysis | | **Business context** | `currency`, `coupon_code`, `referrer` | Revenue and attribution | ### What NOT to include * **PII in plain text** — use the [redact transform](/pipeline/transforms/redact) or hash values before sending * **High-cardinality IDs** — don't put `request_id` or `trace_id` in event properties (use logs for that) * **Redundant context** — the SDK already sends device type, OS, browser, and session ID automatically ### Super properties If you find yourself adding the same property to every `track` call, register it once: ```javascript theme={null} tell.register({ app_version: '2.1.0', environment: 'production' }); ``` Every subsequent event includes these properties automatically. See [Events & Properties](/tracking/events-and-properties) for details. ## Structuring logs Logs need a **severity level** and a **message**. The **service name** is set once when you initialize the SDK. Add structured data for anything you'd want to filter or search on. ```javascript theme={null} tell.log('error', 'Payment gateway timeout', { gateway: 'stripe', duration_ms: 12340, retry_count: 3 }); ``` Use a consistent service name for each SDK instance — `billing`, `auth`, `api`, `worker`. This becomes your primary filter dimension in log queries. See [Logs](/tracking/logs) for severity levels and SDK methods. ## A starter tracking plan Here's a minimal set of events that covers most SaaS products: | Event | Properties | Why | | - | - | - | | `page_view` | `url`, `title`, `referrer` | Traffic and navigation | | `sign_up` | `plan`, `source`, `referrer` | Acquisition | | `login` | `method` | Activation and frequency | | `feature_used` | `feature`, `context` | Engagement | | `purchase` | `plan`, `amount`, `currency` | Revenue | | `invite_sent` | `role` | Virality | | `export_created` | `format`, `row_count` | Value delivery | Start small. You can always add events later — but renaming or removing them means losing historical continuity. ## What's next * [Events & Properties](/tracking/events-and-properties) — the full event model and SDK methods * [Logs](/tracking/logs) — structured logging with severity levels * [Integrations](/tracking/integrations/overview) — pull snapshots from external services * [Filtering](/analytics/filtering) — how properties become filters and breakdowns # Pipeline Configuration Source: https://docs.tell.rs/pipeline/configuration TOML configuration for sources, sinks, transforms, and routing. Tell's pipeline — sources, routing, transforms, and sinks — is configured in a single TOML file. The defaults work for most setups, so you only need to add what you want to change. ## Minimal config A working pipeline with one source and one sink: ```toml theme={null} [[sources.tcp]] port = 50000 [sinks.clickhouse] host = "localhost:8123" database = "tell" [routing] default = ["clickhouse"] ``` This accepts SDK data on TCP port 50000 and writes it to ClickHouse. Everything else uses defaults. ## Sources Sources define where data comes in. You can run multiple sources at the same time. ### TCP The primary source for SDK data. SDKs send FlatBuffer batches over TCP. ```toml theme={null} [[sources.tcp]] port = 50000 ``` Common options: | Field | Default | Notes | | - | - | - | | `port` | 50000 | Listen port (required) | | `address` | `"::"` | Bind address (IPv4+IPv6) | | `flush_interval` | `"100ms"` | Batch flush interval | | `max_connections` | 10000 | Max concurrent connections | | `forwarding_mode` | false | Trust upstream Tell instances | ### HTTP HTTP source for web apps and the JavaScript SDK — supports JSONL and binary ingestion. ```toml theme={null} [sources.http] port = 8080 ``` | Field | Default | Notes | | - | - | - | | `port` | 8080 | Listen port | | `max_payload_size` | 10MB | Max request body | | `cors_enabled` | false | Enable for browser clients | | `trust_proxy` | false | Trust X-Forwarded-For | | `tls_cert_path` | — | TLS certificate path | | `tls_key_path` | — | TLS private key path | ### Syslog Collect logs from syslog-compatible systems (RFC 3164/5424). ```toml theme={null} [sources.syslog_tcp] port = 514 workspace_id = "1" [sources.syslog_udp] port = 514 workspace_id = "1" ``` Syslog sources require a `workspace_id` since syslog clients don't authenticate with API keys. ## Sinks Sinks define where data goes. Each sink has a name and a type. ### ClickHouse (recommended) ```toml theme={null} [sinks.clickhouse] host = "localhost:8123" database = "tell" username = "default" password = "" ``` | Field | Default | Notes | | - | - | - | | `host` | — | ClickHouse HTTP address (required) | | `database` | `"default"` | Database name | | `batch_size` | 50000 | Rows per insert | | `flush_interval` | `"5s"` | Flush interval | ### Disk Binary and plaintext file sinks for local storage. ```toml theme={null} [sinks.logs] type = "disk_plaintext" path = "/var/log/tell" rotation = "daily" compression = "lz4" ``` | Field | Default | Notes | | - | - | - | | `path` | — | Output directory (required) | | `rotation` | `"daily"` | `"hourly"` or `"daily"` | | `compression` | `"none"` | `"none"` or `"lz4"` | ### Parquet Columnar storage for data warehousing. ```toml theme={null} [sinks.archive] type = "parquet" path = "/data/parquet" compression = "snappy" ``` Compression options: `snappy`, `zstd`, `lz4`, `uncompressed`. ### Arrow IPC Fast columnar storage for hot data — readable with DuckDB, PyArrow, or Polars. ```toml theme={null} [sinks.hot] type = "arrow_ipc" path = "/data/arrow" rotation = "hourly" ``` ### Forwarder Send data to another Tell instance for edge-to-cloud deployments. ```toml theme={null} [sinks.upstream] type = "forwarder" target = "central-tell.example.com:50000" api_key = "abcdef0123456789abcdef0123456789" ``` The `api_key` must be exactly 32 hex characters. ## Routing Routing connects sources to sinks. Data from a source goes through matching rules and is delivered to the configured sinks. ```toml theme={null} [routing] default = ["clickhouse"] [[routing.rules]] match = { source_type = "syslog" } sinks = ["clickhouse", "logs"] [[routing.rules.transformers]] type = "pattern_matcher" ``` * `default` — sinks for traffic that doesn't match any rule * `match` — filter by `source` (exact name) or `source_type` (`"tcp"`, `"syslog"`) * `sinks` — where to send matched data (must exist in `[sinks]`) * `transformers` — transforms to apply in order before writing See [Routing](/pipeline/routing) for match logic and [Transforms](/pipeline/transforms/overview) for available transformers. ## Transforms Transforms modify data in routing rules before it reaches sinks. ```toml theme={null} [[routing.rules.transformers]] type = "redact" strategy = "hash" hash_key = "your-secret" patterns = ["email", "ipv4"] ``` Available types: `pattern_matcher`, `redact`, `filter`, `reduce`. See [Transforms](/pipeline/transforms/overview) for configuration details. ## Global defaults Tune pipeline-wide defaults in `[global]`: ```toml theme={null} [global] batch_size = 500 queue_size = 1000 shutdown_timeout_secs = 2 ``` Most users don't need to change these. # Pipeline Overview Source: https://docs.tell.rs/pipeline/overview Sources, routing, transforms, and sinks — Tell's streaming data pipeline. Tell's pipeline moves data from your apps to storage in real time. SDKs and services send events, logs, and sessions to **sources**. The **router** matches data to one or more **sinks** based on rules you define, optionally applying **transforms** along the way. ``` Sources Router Sinks TCP ───┐ ┌─── ClickHouse HTTP ───┼─── routing rules ──┼─── Disk Syslog ───┘ + transforms └─── Parquet ``` ## How data flows 1. **Sources** accept incoming data — TCP binary for native SDKs (the Tell protocol, fastest path), HTTP with JSONL or binary support for web apps, syslog for infrastructure logs 2. **Routing rules** match each batch by source name or type and determine which sinks receive it 3. **Transforms** (optional) modify data in-flight — redact PII, extract log patterns, filter events 4. **Sinks** write data to storage — ClickHouse for analytics, disk for archival, or forward to another Tell instance A batch can go to multiple sinks simultaneously (fan-out). Tell delivers to each sink without copying the data, so fan-out to 5 sinks costs roughly the same as writing to 1. ## Minimal configuration A working pipeline needs one source, one sink, and a routing rule: ```toml theme={null} [[sources.tcp]] port = 50000 [sinks.clickhouse] host = "localhost:8123" database = "tell" [routing] default = ["clickhouse"] ``` See [Pipeline Configuration](/pipeline/configuration) for the full config reference. ## Sources Sources listen for incoming data. You can run multiple sources at the same time. | Source | Use case | | - | - | | [TCP](/pipeline/sources/tcp) | Native SDK ingestion — binary Tell protocol, highest throughput (recommended) | | [HTTP](/pipeline/sources/http) | Web apps and JavaScript SDK — JSONL and binary support | | [Syslog](/pipeline/sources/syslog) | Infrastructure logs (RFC 3164/5424) | ## Routing Routing rules decide which sinks receive which data. Rules match by source name or source type. The first matching rule wins. Unmatched data goes to the default sinks. ```toml theme={null} [routing] default = ["clickhouse"] [[routing.rules]] match = { source_type = "syslog" } sinks = ["clickhouse", "logs"] ``` See [Routing](/pipeline/routing) for matching logic and examples. ## Transforms Transforms process data between routing and sinks. They're configured per routing rule and applied in order. | Transform | What it does | | - | - | | Pattern extraction | Cluster logs into patterns using the Drain algorithm | | Redact | Scrub PII with 11 built-in patterns — email, phone, IP, and more | | Filter | Drop or keep events by condition | | Reduce | Consolidate similar events with count metadata | See [Transforms](/pipeline/transforms/overview) for configuration details. ## Sinks Sinks write data to storage or forward it elsewhere. | Sink | Use case | | - | - | | ClickHouse | Production analytics (recommended) | | Disk | Binary or plaintext file storage | | Parquet | Columnar archival with compression | | Arrow IPC | Fast local storage for Polars/DuckDB | | Forwarder | Edge-to-cloud Tell-to-Tell relay | | Stdout | Debugging | ## Backpressure If a sink can't keep up, Tell drops batches for that sink rather than blocking the entire pipeline. Other sinks continue receiving data normally. Backpressure events are tracked in pipeline metrics and logged — check `tell status --metrics` to spot sinks that are falling behind. ## Graceful shutdown When Tell shuts down, it finishes processing batches already in the pipeline before stopping. Sources stop accepting new connections, in-flight batches drain to sinks, and final metrics are logged. # Routing Source: https://docs.tell.rs/pipeline/routing Route data from any source to any combination of sinks. Routing controls where your data goes. Each batch from a source is matched against rules you define, and the matched rule determines which sinks receive it. You can send the same data to multiple sinks (fan-out) and attach transforms to specific routes. ## Quick start The simplest setup sends everything to one sink: ```toml theme={null} [routing] default = ["clickhouse"] ``` All data from all sources goes to the ClickHouse sink. No rules needed. ## Adding rules Rules match data by source and send it to specific sinks. Rules are evaluated top to bottom — the first match wins. ```toml theme={null} [routing] default = ["clickhouse"] [[routing.rules]] match = { source_type = "syslog" } sinks = ["clickhouse", "logs"] [[routing.rules]] match = { source = "tcp_debug" } sinks = ["stdout"] ``` In this example: * Syslog data (both TCP and UDP) goes to ClickHouse **and** the disk sink — fan-out to two destinations * The debug source goes to stdout only * Everything else (TCP from SDKs, HTTP) falls through to the default: ClickHouse ## Match conditions Each rule has a `match` object with one or both of these fields: | Field | Matches | | - | - | | `source` | Exact source name — `"tcp"`, `"tcp_debug"`, `"syslog_tcp"`, `"syslog_udp"`, `"http"` | | `source_type` | Source type — `"tcp"` matches all TCP sources, `"syslog"` matches both syslog TCP and UDP | If you specify both fields, both must match (AND logic). ```toml theme={null} # Match only the primary TCP source [[routing.rules]] match = { source = "tcp" } sinks = ["clickhouse"] # Match all syslog sources (tcp + udp) [[routing.rules]] match = { source_type = "syslog" } sinks = ["clickhouse", "logs"] ``` ## Evaluation order 1. Rules are checked in the order they appear in the config file 2. The **first matching rule** wins — no further rules are evaluated 3. If no rule matches, data goes to the `default` sinks 4. If no rule matches and `default` is empty, the data is **dropped** Put specific rules (matching exact source names) before broad rules (matching source types) to avoid early matches swallowing everything. ## Fan-out A single rule can send data to multiple sinks: ```toml theme={null} [[routing.rules]] match = { source_type = "syslog" } sinks = ["clickhouse", "disk_logs", "forwarder"] ``` Tell delivers to all three sinks without copying the data. If one sink is slow or down, the others still receive their copy — fan-out is independent per sink. ## Adding transforms Attach transforms to a routing rule to process data before it reaches sinks. Transforms run in order. ```toml theme={null} [[routing.rules]] match = { source_type = "syslog" } sinks = ["clickhouse", "logs"] [[routing.rules.transformers]] type = "pattern_matcher" [[routing.rules.transformers]] type = "redact" strategy = "hash" hash_key = "your-secret" patterns = ["email", "ipv4"] ``` This extracts log patterns first, then redacts PII, then writes to both sinks. See [Transforms](/pipeline/transforms/overview) for available transform types and their configuration. ## Common patterns **Analytics + archival:** Send SDK data to ClickHouse for queries and Parquet for long-term storage. ```toml theme={null} [[routing.rules]] match = { source = "tcp" } sinks = ["clickhouse", "archive"] ``` **Separate log handling:** Route syslog through pattern extraction to ClickHouse, while SDK events skip transforms. ```toml theme={null} [routing] default = ["clickhouse"] [[routing.rules]] match = { source_type = "syslog" } sinks = ["clickhouse"] [[routing.rules.transformers]] type = "pattern_matcher" ``` **Edge forwarding:** Forward all data to a central Tell server. ```toml theme={null} [routing] default = ["upstream"] ``` # Arrow IPC Sink Source: https://docs.tell.rs/pipeline/sinks/arrow-ipc Fast columnar storage for hot data with zero-copy reads for Polars, DuckDB, and DataFusion. The Arrow IPC sink writes data to Arrow IPC files (also known as Feather) for fast I/O and zero-copy reads. Use it for hot data that needs frequent access — real-time dashboards, local development, and inter-process communication. ## Configuration ```toml theme={null} [sinks.hot] type = "arrow_ipc" path = "/data/arrow" rotation = "daily" ``` | Field | Default | Notes | | - | - | - | | `path` | — | Output directory (required) | | `rotation` | `"daily"` | `"hourly"` or `"daily"` | | `buffer_size` | 10,000 | Rows buffered before flush | | `flush_interval` | `"60s"` | Time-based flush interval | ## Tables The Arrow IPC sink writes the full Tell v1.1 schema — the same 7 tables as the ClickHouse sink: | File | Populated by | | - | - | | `events_v1.arrow` | TRACK events | | `logs_v1.arrow` | Log entries | | `snapshots_v1.arrow` | Integration snapshots | | `context_v1.arrow` | CONTEXT events (device/location) | | `users_v1.arrow` | IDENTIFY events (core identity) | | `user_devices.arrow` | IDENTIFY events (device links) | | `user_traits.arrow` | IDENTIFY events (key-value traits) | Like the ClickHouse sink, IDENTIFY events fan out to `users_v1`, `user_devices`, and `user_traits`. CONTEXT events extract device and location fields from the payload. ## File organization ``` arrow_ipc/ └── {workspace_id}/ └── {date}/ ├── events_v1.arrow ├── logs_v1.arrow ├── context_v1.arrow ├── users_v1.arrow ├── user_devices.arrow └── user_traits.arrow ``` ## Reading Arrow IPC files Arrow IPC is supported natively by all major data tools: ```python theme={null} # Polars (recommended — zero-copy read) import polars as pl df = pl.read_ipc("arrow_ipc/1/2025-01-15/events_v1.arrow") # DuckDB SELECT * FROM 'arrow_ipc/1/2025-01-15/events_v1.arrow'; # PyArrow import pyarrow.ipc as ipc reader = ipc.open_file("arrow_ipc/1/2025-01-15/events_v1.arrow") ``` Also supported by DataFusion (native) and any Arrow-compatible tool. ## Local query backend Tell's local query engine (Polars backend) reads from Arrow IPC files directly. Configure this in `[query]`: ```toml theme={null} [query] backend = "polars" data_dir = "/data/arrow" ``` This is useful for development and small deployments where ClickHouse isn't needed. # ClickHouse Sink Source: https://docs.tell.rs/pipeline/sinks/clickhouse Production analytics sink with event-type routing, per-table batching, and concurrent flushes. The ClickHouse sink is the recommended production destination. It routes events to the appropriate tables based on event type, batches inserts per table, and flushes all tables concurrently. ## Configuration ```toml theme={null} [sinks.clickhouse] host = "localhost:8123" database = "tell" username = "default" password = "" ``` | Field | Default | Notes | | - | - | - | | `host` | — | ClickHouse HTTP address (required) | | `database` | `"default"` | Database name | | `username` | `"default"` | Authentication username | | `password` | `""` | Authentication password | | `batch_size` | 1000 | Rows per table before flush | | `flush_interval` | `"5s"` | Time-based flush interval | ## Tables The sink writes to 7 tables following the Tell v1.1 schema: | Table | Populated by | | - | - | | `events_v1` | TRACK events from SDKs | | `users_v1` | IDENTIFY events (core identity) | | `user_devices` | IDENTIFY events (device links) | | `user_traits` | IDENTIFY events (key-value traits) | | `context_v1` | CONTEXT events (device/location) | | `logs_v1` | Log entries from syslog and SDKs | | `snapshots_v1` | Integration snapshots (GitHub, Shopify, etc.) | IDENTIFY events fan out to three tables in parallel: `users_v1`, `user_devices`, and `user_traits`. ## Performance * **Per-table batching** — each table accumulates rows independently, so a burst of logs doesn't delay event flushes * **Concurrent flush** — all tables are flushed in parallel using tokio tasks * **LZ4 compression** — enabled by default for wire transport * **Retry with backoff** — 3 attempts with exponential backoff (100ms base, 10s max) on transient failures ## Arrow HTTP transport Two implementations are available: * **Row-based** (default) — uses the clickhouse crate with row structs * **Arrow-based** — sends Arrow-format HTTP inserts for higher throughput The Arrow transport is selected automatically when available and provides better performance at high event volumes. ## Setup Tell can create the ClickHouse schema automatically: ```bash theme={null} tell setup-db --host localhost:8123 --database tell ``` This creates the database, all 7 tables, and the default user. See [ClickHouse Setup](/running-tell/clickhouse) for details. # Disk Sink Source: https://docs.tell.rs/pipeline/sinks/disk Binary and plaintext file storage with atomic rotation and optional LZ4 compression. Disk sinks write data to local files. Two types are available: `disk_binary` for high-throughput binary storage and `disk_plaintext` for human-readable logs. ## Plaintext Human-readable log files, split by data type. Easy to grep, tail, and debug. ```toml theme={null} [sinks.logs] type = "disk_plaintext" path = "/var/log/tell" rotation = "daily" compression = "lz4" ``` | Field | Default | Notes | | - | - | - | | `path` | — | Output directory (required) | | `rotation` | `"daily"` | `"hourly"` or `"daily"` | | `compression` | `"none"` | `"none"` or `"lz4"` (70-85% smaller) | ### Output format Events and logs are decoded from FlatBuffer format and written as structured text: ``` [2025-01-15T10:30:45.123Z] [TRACK] device_id=abc123 session_id=xyz789 event_name=button_click source_ip=192.168.1.100 payload={...} [2025-01-15T10:30:45.123Z] [INFO] session_id=xyz789 source=api service=auth source_ip=192.168.1.100 payload=request completed ``` ### Directory structure Files are separated by type within each workspace and date: ``` logs/ └── workspace_42/ └── 2025-01-15/ ├── events.log ├── logs.log ├── snapshots.log └── raw.log # failed decodes only ``` ## Binary High-performance binary storage with 24-byte metadata headers. Use this for archival when you need maximum throughput and plan to read with `tell read`. ```toml theme={null} [sinks.archive] type = "disk_binary" path = "/data/archive" rotation = "hourly" compression = "lz4" ``` | Field | Default | Notes | | - | - | - | | `path` | — | Output directory (required) | | `rotation` | `"hourly"` | `"hourly"` or `"daily"` | | `compression` | `"none"` | `"none"` or `"lz4"` (60-80% smaller) | | `buffer_size` | 32MB | Write buffer size | ### File format Each message is stored as: ``` [24-byte metadata][4-byte size][FlatBuffer data] ``` Metadata contains the batch timestamp (8 bytes, Unix ms) and source IP (16 bytes, IPv6 format). Read binary files with `tell read`: ```bash theme={null} tell read /data/archive/1/2025-01-15/10/data.bin ``` ## Atomic rotation Both disk sinks use atomic rotation for zero data loss during file switches. The hot write path is lock-free and non-blocking. File rotation runs on a separate task and checks every 60 seconds. If the write queue fills up (sink can't keep up), batches are dropped for that sink and logged as backpressure events. # Forwarder Sink Source: https://docs.tell.rs/pipeline/sinks/forwarder Tell-to-Tell forwarding for edge-to-cloud deployments with retry and reconnection. The forwarder sink sends data to another Tell instance over TCP. Use it for edge-to-cloud deployments where edge nodes collect data locally and relay it to a central server. ## Configuration ```toml theme={null} [sinks.upstream] type = "forwarder" target = "central-tell.example.com:50000" api_key = "abcdef0123456789abcdef0123456789" ``` | Field | Default | Notes | | - | - | - | | `target` | — | Remote Tell server address as `host:port` (required) | | `api_key` | — | 32-character hex streaming key for the remote server (required) | ## How it works The forwarder receives batches from the router, rebuilds each FlatBuffer message with: * The remote server's API key (replacing the local key) * The original client's source IP preserved in the `source_ip` field The remote Tell server receives the data as if the original client connected directly, preserving IP-based analytics. ## Protocol The forwarder uses the same length-prefixed FlatBuffer protocol as Tell's TCP source: ``` [4 bytes: length (big-endian)][N bytes: FlatBuffer batch] ``` The receiving server must have `forwarding_mode = true` on its TCP source to honor the `source_ip` field: ```toml theme={null} # On the central server [[sources.tcp]] port = 50000 forwarding_mode = true ``` Without `forwarding_mode`, the central server would record the edge node's IP instead of the original client's IP. ## Reliability * **Auto-reconnect** — reconnects automatically if the connection drops, with configurable retry intervals * **Retry with backoff** — 3 retry attempts per message with 1-second intervals * **TCP keep-alive** — enabled by default (30-second interval) to detect dead connections * **Per-message forwarding** — if one message fails, remaining messages in the batch still attempt delivery ## Edge deployment pattern A typical edge-to-cloud setup: ```toml theme={null} # Edge node config [[sources.tcp]] port = 50000 [sinks.upstream] type = "forwarder" target = "central.example.com:50000" api_key = "abcdef0123456789abcdef0123456789" [routing] default = ["upstream"] ``` SDKs connect to the edge node. The edge node forwards everything to the central server. You can also add local sinks for redundancy: ```toml theme={null} [routing] default = ["upstream", "local_archive"] ``` # Sinks Overview Source: https://docs.tell.rs/pipeline/sinks/overview Write data to ClickHouse, Vortex, Parquet, Arrow IPC, disk, or forward to another Tell instance. Sinks receive data from the router and write it to storage. Each sink runs independently — if one is slow or down, the others continue unaffected. ## Available sinks | Sink | Type | Use case | | - | - | - | | [ClickHouse](/pipeline/sinks/clickhouse) | `clickhouse` | Production analytics (recommended) | | [Disk](/pipeline/sinks/disk) | `disk_binary`, `disk_plaintext` | Binary or plaintext file storage | | [Parquet](/pipeline/sinks/parquet) | `parquet` | Columnar archival with compression | | [Arrow IPC](/pipeline/sinks/arrow-ipc) | `arrow_ipc` | Fast local storage for Polars/DuckDB | | [Vortex](/pipeline/sinks/vortex) | `vortex` | Fast columnar reads with cascading compression | | [Forwarder](/pipeline/sinks/forwarder) | `forwarder` | Edge-to-cloud Tell-to-Tell relay | | Stdout | `stdout` | Debug output (development only) | | Null | `null` | Benchmarking (discards all data) | ## Choosing a sink **ClickHouse** is the recommended production sink. It handles event-type routing, per-table batching, concurrent flushes, and retry logic. The query engine connects directly to ClickHouse for analytics. **Disk** sinks are for local file storage. Use `disk_binary` for high-throughput archival with optional LZ4 compression, or `disk_plaintext` for human-readable logs you can grep and tail. **Parquet** is for data warehousing. Files are readable by Spark, DuckDB, Pandas, and Polars. Good for cold storage with excellent compression ratios. **Arrow IPC** is for hot data that needs frequent access. \~10x faster reads than Parquet, with zero-copy memory mapping. Ideal for real-time dashboards backed by Polars or DuckDB. **Vortex** is for fast analytical reads without a database. \~100x faster random access than Parquet via cascading columnar encodings. Use it when you need ClickHouse-class query speed on local files. **Forwarder** sends data to another Tell instance over TCP. Use this for edge-to-cloud deployments where edge nodes collect data and relay it to a central server. ## Performance Measured with `tell-bench sink` — 10M events, realistic cardinality (1000 devices, 25 event types, unique payloads), Apple M4 Pro: | Sink | Events/s | Written | Ratio | Best for | | - | - | - | - | - | | disk\_binary | 33.0M | 639 MB | 0.36x | Maximum write speed | | disk\_binary\_lz4 | 21.6M | 145 MB | 0.08x | Speed + compression | | arrow\_ipc | 2.9M | 1.7 GB | 0.98x | Fast reads (Polars/DuckDB) | | parquet\_lz4 | 2.6M | 251 MB | 0.14x | Speed/compression balance | | parquet\_zstd | 2.3M | 171 MB | 0.10x | Cold archival (best ratio) | | parquet\_uncompressed | 2.6M | 769 MB | 0.43x | Columnar without codec overhead | | vortex | 1.7M | 1.8 GB | 0.99x | Fast random access + scans | | disk\_plaintext | 1.4M | 2.7 GB | 1.53x | Human-readable grep/tail | **Ratio** = bytes on disk / input bytes. Lower is better compression. Parquet's columnar layout eliminates per-event FlatBuffer overhead, so even "uncompressed" Parquet (0.43x) is smaller than raw binary (0.36x at full throughput). ## Backpressure If a sink can't keep up, Tell drops batches for that sink rather than blocking the pipeline. Other sinks continue receiving data normally. Dropped batches are tracked in pipeline metrics — check `tell status --metrics` to spot sinks falling behind. ## File rotation Disk-based sinks (disk, Parquet, Arrow IPC, Vortex) organize files by workspace and time: ``` {path}/{workspace_id}/{date}/{hour}/ ``` Rotation can be `hourly` or `daily`. Disk sinks use atomic rotation to guarantee zero data loss during file switches. # Parquet Sink Source: https://docs.tell.rs/pipeline/sinks/parquet Columnar storage for data warehousing with Snappy, Zstd, or LZ4 compression. The Parquet sink writes data to Apache Parquet files — a columnar format optimized for analytical queries with excellent compression ratios. Use it for cold storage and data warehousing. ## Configuration ```toml theme={null} [sinks.archive] type = "parquet" path = "/data/parquet" compression = "snappy" rotation = "hourly" ``` | Field | Default | Notes | | - | - | - | | `path` | — | Output directory (required) | | `compression` | `"snappy"` | `"snappy"`, `"zstd"`, `"lz4"`, or `"uncompressed"` | | `rotation` | `"hourly"` | `"hourly"` or `"daily"` | | `buffer_size` | 10,000 | Rows buffered before flush | | `row_group_size` | 100,000 | Max rows per row group | | `flush_interval` | `"60s"` | Time-based flush interval | ## Schemas Data is separated into different Parquet files by type, each with an optimized schema. Fields are ordered for predicate pushdown — frequently filtered columns come first, large payloads last. **Events** (9 fields): `timestamp`, `batch_timestamp`, `workspace_id`, `event_type`, `event_name`, `device_id`, `session_id`, `source_ip`, `payload` **Logs** (10 fields): `timestamp`, `batch_timestamp`, `workspace_id`, `level`, `event_type`, `source`, `service`, `session_id`, `source_ip`, `payload` **Snapshots** (7 fields): `timestamp`, `batch_timestamp`, `workspace_id`, `source`, `entity`, `source_ip`, `payload` ## File organization ``` parquet/ └── {workspace_id}/ └── {date}/ └── {hour}/ ├── events.parquet ├── logs.parquet └── snapshots.parquet ``` ## Reading Parquet files Parquet is a standard format readable by most analytics tools: ```python theme={null} # DuckDB SELECT * FROM 'parquet/1/2025-01-15/10/*.parquet'; # Polars import polars as pl df = pl.read_parquet("parquet/1/2025-01-15/10/events.parquet") # Pandas import pandas as pd df = pd.read_parquet("parquet/1/2025-01-15/10/events.parquet") ``` Also readable by Apache Spark, ClickHouse (`file()` table function), and PyArrow. ## When to use Parquet vs Arrow IPC Use **Parquet** for cold data — archival, compliance, data warehousing. Compression ratios are excellent (especially with Zstd) and the format is widely supported. Use **[Arrow IPC](/pipeline/sinks/arrow-ipc)** for hot data — real-time dashboards, frequent reads, inter-process communication. Arrow IPC is \~10x faster to read but files are larger. # Vortex Sink Source: https://docs.tell.rs/pipeline/sinks/vortex Next-gen columnar storage with cascading compression — 100x faster random access than Parquet. The Vortex sink writes data to Vortex files (`.vtx`) — a columnar format that delivers 100x faster random access and 10–20x faster sequential scans than Parquet, with comparable compression ratios. Use it when you need fast analytical queries on recent data without a database. ## Configuration ```toml theme={null} [sinks.analytics] type = "vortex" path = "/data/vortex" rotation = "hourly" ``` | Field | Default | Notes | | - | - | - | | `path` | — | Output directory (required) | | `rotation` | `"hourly"` | `"hourly"` or `"daily"` | | `buffer_size` | 10,000 | Rows buffered before flush | | `flush_interval` | `"60s"` | Time-based flush interval | Compression is automatic — Vortex selects the optimal encoding per column using cascading strategies (dictionary, run-length, ALP for timestamps, FSST for strings, Zstd as a final stage). There's nothing to configure. ## Tables The Vortex sink writes the full Tell schema — 10 tables covering events, logs, identity, and group data: | File | Populated by | | - | - | | `events.vtx` | TRACK events | | `logs.vtx` | Log entries | | `snapshots.vtx` | Integration snapshots | | `sessions.vtx` | CONTEXT events (device, location, session metadata) | | `users.vtx` | IDENTIFY events (core identity) | | `user_devices.vtx` | IDENTIFY events (device-to-user links) | | `user_traits.vtx` | IDENTIFY events (key-value traits) | | `groups.vtx` | GROUP events (core group identity) | | `user_groups.vtx` | GROUP events (user-to-group links) | | `group_traits.vtx` | GROUP events (key-value group traits) | IDENTIFY events fan out to `users`, `user_devices`, and `user_traits`. GROUP events fan out to `groups`, `user_groups`, and `group_traits`. CONTEXT events extract device and location fields into `sessions`. Fields in every table are ordered for predicate pushdown — frequently filtered columns (timestamp, workspace\_id) come first, large payloads last. ## File organization ``` vortex/ └── {workspace_id}/ └── {date}/ └── {hour}/ ├── events.vtx ├── logs.vtx ├── snapshots.vtx ├── sessions.vtx ├── users.vtx ├── user_devices.vtx ├── user_traits.vtx ├── groups.vtx ├── user_groups.vtx └── group_traits.vtx ``` With daily rotation, the `{hour}/` level is omitted. ## Flushing Data is flushed to disk in three situations: 1. **Buffer threshold** — when a workspace/time bucket accumulates `buffer_size` rows 2. **Periodic flush** — every `flush_interval` (default 60s), regardless of buffer fill 3. **Shutdown** — graceful shutdown flushes all remaining buffers Events and logs use zero-allocation builders on the hot path — no per-event heap allocation. Cold-path tables (users, groups, sessions) buffer rows in memory and convert to Arrow on flush. ## When to use Vortex Use **Vortex** when you need fast analytical reads without running ClickHouse — random access is \~100x faster than Parquet and sequential scans are 10–20x faster. The trade-off is larger files on disk (0.99x ratio vs Parquet's 0.10–0.14x) and slower writes (1.7M events/s vs Parquet's 2.3–2.6M). Use **[Parquet](/pipeline/sinks/parquet)** for cold archival where compression matters more than read speed. Parquet files are also more widely supported by third-party tools. Use **[Arrow IPC](/pipeline/sinks/arrow-ipc)** for local development and inter-process communication where zero-copy reads matter and you're already using Polars or DuckDB. ## What's next * [Sinks overview](/pipeline/sinks/overview) — compare all available sinks * [Parquet sink](/pipeline/sinks/parquet) — columnar archival with Snappy/Zstd/LZ4 compression * [Routing](/pipeline/routing) — control which sources send data to which sinks # File Source Source: https://docs.tell.rs/pipeline/sources/file Import data from files — raw lines, structured JSON, or binary re-imports. Read events, logs, or raw lines from a file and push them into Tell's pipeline. Use this for one-time imports, replaying exported data, or ingesting log files from disk. ```toml theme={null} [sources.file] path = "/var/log/app.log" mode = "lines" ``` Tell reads the file line by line, batches the content, and routes it through the pipeline like any other source. ## Read modes The file source supports three modes, each producing different batch types: | Mode | Output | Use case | | - | - | - | | `lines` | Syslog batches | Raw log files, one line per message | | `jsonl` | Log or Event batches | Structured JSON — one JSON object per line | | `binary` | Original batches | Re-import data previously exported by the [disk sink](/pipeline/sinks/disk) | ## Lines mode Each line becomes a syslog message. Empty lines are skipped. ```toml theme={null} [sources.file] path = "/var/log/nginx/access.log" mode = "lines" one_shot = true ``` This reads the entire file, batches every 500 lines, and exits. Use [transforms](/pipeline/transforms/overview) downstream to parse structured fields from raw lines. ## JSONL mode Parse each line as a JSON object and produce typed batches. Set `format` to control the schema: ### Log format ```toml theme={null} [sources.file] path = "logs.jsonl" mode = "jsonl" format = "log" service = "api" ``` Each line is a JSON log entry: ```json theme={null} {"level": "error", "message": "connection refused", "service": "api", "timestamp": 1718467200000} {"level": "info", "message": "request completed", "data": {"status": 200, "duration_ms": 42}} ``` | Field | Type | Required | Description | | - | - | - | - | | `message` | string | Yes | Log message text. | | `level` | string | No | `debug`, `info`, `warn`, `error`. Default: `info`. | | `timestamp` | integer | No | Unix milliseconds. Default: current time. | | `source` | string | No | Source location (e.g., `app.js:42`). | | `service` | string | No | Service name. Falls back to config `service` value. | | `session_id` | string | No | Session UUID. | | `type` | string | No | Log type. Default: `log`. | | `data` | object | No | Additional properties merged into the payload. | ### Event format ```toml theme={null} [sources.file] path = "events.jsonl" mode = "jsonl" format = "event" ``` Each line is a JSON event: ```json theme={null} {"type": "page_view", "device_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "properties.page": "/pricing"} {"type": "purchase", "device_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "properties.amount": 99} ``` | Field | Type | Required | Description | | - | - | - | - | | `type` | string | Yes | Event type. | | `device_id` | string | Yes | Device UUID. | | `event` | string | No | Event name. | | `session_id` | string | No | Session UUID. | | `user_id` | string | No | User identifier. | | `group_id` | string | No | Group identifier. | | `timestamp` | integer | No | Unix milliseconds. Default: current time. | | `service` | string | No | Service name. | Additional fields are flattened into event properties. Parse errors in JSONL mode are logged and skipped — a malformed line won't stop the import. ## Binary mode Re-import data previously exported by the [disk sink](/pipeline/sinks/disk). Tell reconstructs the original batches with their metadata, including source IPs. ```toml theme={null} [sources.file] path = "/data/tell/export/2025-06-15.bin" mode = "binary" ``` This is useful for replaying historical data or migrating between Tell instances. ## One-shot vs continuous By default, the file source reads the file once and exits (`one_shot = true`). Set `one_shot = false` to keep the source running after reading: ```toml theme={null} [sources.file] path = "test-data.jsonl" mode = "jsonl" format = "event" one_shot = false ``` ## Configuration reference ```toml theme={null} [sources.file] path = "" # File path to read (required) mode = "lines" # "lines", "jsonl", or "binary" format = "log" # "log" or "event" (JSONL mode only) workspace_id = "1" # Workspace ID for routing service = "" # Service name for log enrichment batch_size = 500 # Items per batch one_shot = true # Exit after reading the file ``` The file source requires the `source-file` feature flag at build time. ## What's next * [Routing](/pipeline/routing) — control where file data goes after ingestion * [Transforms](/pipeline/transforms/overview) — parse and enrich raw lines before storage * [Disk sink](/pipeline/sinks/disk) — export data that binary mode can re-import # HTTP Source Source: https://docs.tell.rs/pipeline/sources/http HTTP ingestion for the JavaScript SDK and web apps — JSONL and binary support with TLS, CORS, and rate limiting. Web apps can't easily connect to a custom TCP port, so the HTTP source provides an alternative ingestion path with JSONL and binary support. This is what the JavaScript SDK uses. ```toml theme={null} [sources.http] port = 8080 ``` ## Endpoints | Method | Path | Content-Type | Description | | - | - | - | - | | POST | `/v1/events` | `application/json` | Ingest events (JSONL) | | POST | `/v1/logs` | `application/json` | Ingest logs (JSONL) | | POST | `/v1/ingest` | `application/x-flatbuffers` | Binary FlatBuffer batches | | GET | `/health` | — | Returns `{"status": "ok"}` | The JSONL endpoints accept one JSON object per line. The binary endpoint accepts pre-encoded FlatBuffer batches — the same wire format the [TCP source](/pipeline/sources/tcp) uses. Content types `application/octet-stream` and `application/x-tell` are also accepted on the binary endpoint. ## Authentication Three ways to authenticate, in precedence order: 1. `Authorization: Bearer ` header 2. `X-API-Key: ` header 3. `?token=` query parameter The query parameter option exists for browser SDKs that need to avoid CORS preflight requests on simple POST requests. ```bash theme={null} curl -X POST https://tell.example.com/v1/events \ -H "Authorization: Bearer a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6" \ -H "Content-Type: application/json" \ -d '{"type":"track","event":"page_view","device_id":"abc123"}' ``` Keys are 32 hex characters. See [API keys](/running-tell/api-keys) for creating and managing them. ## Responses **202 Accepted** — all items ingested successfully: ```json theme={null} {"accepted": 12, "request_id": "req_a1b2c3"} ``` **207 Multi-Status** — partial success (some items rejected): ```json theme={null} { "accepted": 10, "rejected": 2, "errors": [ {"line": 3, "error": "missing required field: device_id"}, {"line": 7, "error": "invalid event type"} ], "request_id": "req_a1b2c3" } ``` **400 Bad Request** — all items rejected or invalid format. **401 Unauthorized** — missing or invalid API key. **413 Payload Too Large** — request body exceeds `max_payload_size`. **429 Too Many Requests** — rate limited after repeated auth failures. **503 Service Unavailable** — pipeline backpressure (channel full). ## CORS and browser clients CORS is enabled by default so browser SDKs work without extra setup. The source mirrors the request's `Origin` header and allows `Authorization`, `Content-Type`, `X-API-Key`, and `X-Workspace-ID` headers. To restrict allowed origins: ```toml theme={null} [sources.http] port = 8080 cors_origins = ["https://myapp.com", "https://staging.myapp.com"] ``` ## TLS Tell supports HTTPS natively via rustls. Provide PEM-encoded certificate and key files: ```toml theme={null} [sources.http] port = 443 tls_cert_path = "/etc/tell/cert.pem" tls_key_path = "/etc/tell/key.pem" ``` When both paths are set, the source binds as HTTPS. Omit them to run plain HTTP (the default). For production, you can also terminate TLS at a reverse proxy like Caddy instead — see [Reverse proxy](#reverse-proxy) below. ## Reverse proxy Running Caddy in front of Tell is the simplest way to get automatic HTTPS with Let's Encrypt certificates and zero TLS configuration: ``` tell.example.com { reverse_proxy localhost:8080 } ``` Caddy handles certificate provisioning, renewal, and TLS termination automatically. Enable `trust_proxy` in Tell so the real client IP is captured from `X-Forwarded-For`: ```toml theme={null} [sources.http] port = 8080 trust_proxy = true ``` This also works with nginx or any reverse proxy that sets `X-Forwarded-For`. Only enable `trust_proxy` when the HTTP source is behind a trusted proxy — clients can forge the header otherwise. ## Rate limiting Authentication failures are rate-limited per IP. After 10 failures within 60 seconds, the IP is temporarily blocked with `429 Too Many Requests`. The rate limiter cleans up expired entries every 5 minutes. ## Safety limits JSONL endpoints enforce two limits to prevent denial-of-service: * **10,000 lines per request** — requests with more lines are rejected * **32 levels of JSON nesting** — deeply nested objects are rejected before parsing These limits are not configurable. Malformed JSON lines are skipped individually — a single bad line won't reject the entire request (you'll get a 207 response with per-line errors). ## Configuration reference ```toml theme={null} [sources.http] port = 8080 address = "::" # Bind address (dual-stack IPv4+IPv6) max_payload_size = 16777216 # Max request body in bytes (16 MB) batch_size = 500 # Items per internal batch flush_interval = "100ms" # Flush partial batches after this interval request_timeout = "30s" # HTTP request timeout cors_enabled = true # Enable CORS headers cors_origins = [] # Allowed origins (empty = all) request_logging = false # Log every HTTP request trust_proxy = false # Trust X-Forwarded-For header tls_cert_path = "" # PEM certificate path (enables HTTPS) tls_key_path = "" # PEM private key path (enables HTTPS) ``` ## What's next * [TCP source](/pipeline/sources/tcp) — higher throughput for native SDKs * [JavaScript SDK](/tracking/sdks/javascript) — the primary client for the HTTP source * [API keys](/running-tell/api-keys) — create and manage streaming keys * [Routing](/pipeline/routing) — control where HTTP data goes after ingestion # Sources Source: https://docs.tell.rs/pipeline/sources/overview How data enters Tell — pick the right source for your use case. Sources are where data enters Tell's pipeline. SDKs, HTTP clients, and syslog daemons connect to sources, which batch incoming data and pass it to [routing](/pipeline/routing) and [sinks](/pipeline/sinks/overview). Most setups need just the TCP source — it handles SDK traffic at up to 64M events/sec with zero configuration beyond the port. ## Which source to use | Source | Default Port | Auth | Best for | | - | - | - | - | | [TCP](/pipeline/sources/tcp) | 50000 | API key | Native SDKs (Go, Swift, Flutter, C++) — binary Tell protocol, highest throughput | | [HTTP](/pipeline/sources/http) | 8080 | API key | JavaScript SDK, web apps — JSONL and binary support | | [Syslog](/pipeline/sources/syslog) | 514 | None | Infrastructure logs from servers, routers, firewalls | | [File](/pipeline/sources/file) | — | None | One-time imports, replaying exports, ingesting log files from disk | **Starting out?** Use the [TCP source](/pipeline/sources/tcp). It's the default for all native SDKs. Add [HTTP](/pipeline/sources/http) if you have browser clients. Add [Syslog](/pipeline/sources/syslog) when you want infrastructure logs alongside product events. Use [File](/pipeline/sources/file) for one-time data imports or replaying exported data. You can run multiple sources at once — they all feed into the same pipeline. Configure [routing](/pipeline/routing) to control which sources send data to which [sinks](/pipeline/sinks/overview). ## Throughput Benchmarked on Apple M4 Pro, 12 cores, 5 clients, null sink: | Source | Batch 500 | Batch 100 | Batch 10 | | - | - | - | - | | TCP Binary | 64M/s | 55M/s | 6.9M/s | | HTTP FlatBuffers | 24M/s | 8.3M/s | 1.0M/s | | HTTP JSON | 2.1M/s | 2.3M/s | 848K/s | | Syslog TCP | 8.7M/s | 8.5M/s | 8.3M/s | ## Monitoring Check source health with `tell status`: ``` $ tell status Sources: tcp (port 50000) 12 connections 4.2M events 1.8 GB 0 errors http (port 8080) 3 connections 128K events 42 MB 0 errors syslog_udp (port 514) 4 workers 890K packets 320 MB 2 dropped ``` Key metrics: active connections, events received, bytes, errors, auth failures (TCP/HTTP), and dropped messages (syslog). # Syslog Source Source: https://docs.tell.rs/pipeline/sources/syslog Receive infrastructure logs over TCP or UDP using standard syslog protocols. Collect logs from servers, routers, and firewalls using standard syslog. Tell supports both TCP (reliable delivery) and UDP (high-volume, best-effort) with RFC 3164 (BSD) and RFC 5424 (IETF) formats. ## TCP For reliable log delivery with connection-based framing. Messages are newline-delimited. ```toml theme={null} [sources.syslog_tcp] port = 514 ``` Syslog has no authentication — any client that can reach the port can send logs. Restrict access at the network level (firewall rules, private subnets). Port 514 requires root. Use a higher port (e.g., 50514) and redirect with iptables, or run Tell with `CAP_NET_BIND_SERVICE`. ## UDP For high-volume log ingestion where best-effort delivery is acceptable. Multiple workers share the same port using kernel-level load balancing. ```toml theme={null} [sources.syslog_udp] port = 514 ``` When the pipeline is busy, packets are dropped rather than queued — consistent with UDP semantics. If you're seeing drops under load, increase the worker count: ```toml theme={null} [sources.syslog_udp] port = 514 num_workers = 8 ``` ## When to use TCP vs UDP **TCP** when you need every message delivered — audit logs, security events, compliance data. **UDP** when volume matters more than completeness — application logs, metrics, high-traffic syslog servers. You can run both at once on different ports to handle both cases. ## Advanced ### TCP config reference ```toml theme={null} [sources.syslog_tcp] port = 514 address = "::" # Bind address max_message_size = 8192 # Max syslog message (8 KB) connection_timeout = "30s" # Idle connection timeout no_delay = true # TCP_NODELAY flush_interval = "100ms" # Batch flush interval batch_size = 500 # Messages per batch max_connections = 10000 # Connection limit ``` ### UDP config reference ```toml theme={null} [sources.syslog_udp] port = 514 address = "::" # Bind address num_workers = 4 # Parallel UDP workers max_message_size = 8192 # Max syslog message (8 KB) flush_interval = "50ms" # Faster flush for UDP bursts batch_size = 500 # Messages per batch ``` # TCP Source Source: https://docs.tell.rs/pipeline/sources/tcp High-performance binary ingestion for SDKs — the default and fastest way to send data to Tell. SDKs connect over TCP and stream binary batches. This is the fastest ingestion path — 64M events/sec — and the default for all native SDKs (Go, Swift, Flutter, C++). ```toml theme={null} [[sources.tcp]] port = 50000 ``` That's it. The defaults handle most deployments. SDKs authenticate with an [API key](/running-tell/api-keys) embedded in each batch. ## Forwarding mode You can run multiple TCP sources. A common pattern is one port for direct SDK traffic and another for Tell-to-Tell forwarding between regions: ```toml theme={null} [[sources.tcp]] port = 50000 [[sources.tcp]] port = 8081 forwarding_mode = true ``` With `forwarding_mode = true`, the source trusts the original client IP embedded in forwarded batches instead of using the forwarder's connection IP. ## Backpressure If the pipeline can't keep up, batches are dropped rather than blocking. This keeps all connections responsive — no single slow [sink](/pipeline/sinks/overview) stalls ingestion. ## Advanced Full config reference with defaults: ```toml theme={null} [[sources.tcp]] port = 50000 address = "::" # Bind address (default: dual-stack IPv4+IPv6) forwarding_mode = false # Trust source_ip from upstream forwarders no_delay = true # Disable Nagle's algorithm for lower latency keepalive = true # TCP keepalive probes flush_interval = "100ms" # Flush partial batches after this interval batch_size = 500 # Messages per batch before flush max_connections = 10000 # Reject new connections above this limit buffer_size = 1048576 # Read buffer per connection (1 MB) socket_buffer_size = 262144 # OS socket buffer (256 KB) connection_timeout = "0s" # Idle timeout (0 = no timeout) ``` # Filter Transform Source: https://docs.tell.rs/pipeline/transforms/filter Drop or keep events based on field conditions — string, numeric, regex, and existence checks. The filter transform drops or keeps events based on conditions you define. Use it to remove health checks, exclude debug logs, or keep only production traffic before data reaches your sinks. ## Quick start Drop events where `path` equals `/health`: ```toml theme={null} [[routing.rules.transformers]] type = "filter" action = "drop" field = "path" operator = "eq" value = "/health" ``` ## Actions | Action | What happens | | - | - | | `drop` (default) | Remove events that match — keep everything else | | `keep` | Keep only events that match — remove everything else | ## Operators | Operator | Type | Description | | - | - | - | | `eq` | String/Number | Equal to value | | `ne` | String/Number | Not equal to value | | `contains` | String | Contains substring | | `starts_with` | String | Starts with prefix | | `ends_with` | String | Ends with suffix | | `regex` | String | Matches regex pattern | | `exists` | Any | Field exists (no value needed) | | `gt` | Numeric | Greater than | | `gte` | Numeric | Greater than or equal | | `lt` | Numeric | Less than | | `lte` | Numeric | Less than or equal | ## Multiple conditions When you need more than one condition, use the `conditions` array and set `match` to control how they combine: ```toml theme={null} [[routing.rules.transformers]] type = "filter" action = "drop" match = "any" [[routing.rules.transformers.conditions]] field = "path" operator = "eq" value = "/health" [[routing.rules.transformers.conditions]] field = "path" operator = "starts_with" value = "/metrics" ``` | Match mode | Logic | | - | - | | `all` (default) | All conditions must match (AND) | | `any` | Any condition can match (OR) | ## Examples **Keep only errors from production:** ```toml theme={null} [[routing.rules.transformers]] type = "filter" action = "keep" match = "all" [[routing.rules.transformers.conditions]] field = "level" operator = "eq" value = "error" [[routing.rules.transformers.conditions]] field = "env" operator = "eq" value = "production" ``` **Drop events matching a regex:** ```toml theme={null} [[routing.rules.transformers]] type = "filter" action = "drop" field = "user_agent" operator = "regex" value = "bot|crawler|spider" ``` **Keep events where a user ID exists:** ```toml theme={null} [[routing.rules.transformers]] type = "filter" action = "keep" field = "user_id" operator = "exists" ``` **Filter on nested fields** (dot notation): ```toml theme={null} [[routing.rules.transformers]] type = "filter" action = "drop" field = "request.headers.x-internal" operator = "exists" ``` ## Reference | Field | Default | Description | | - | - | - | | `type` | — | `"filter"` | | `action` | `"drop"` | `"drop"` or `"keep"` | | `match` | `"all"` | `"all"` (AND) or `"any"` (OR) | | `field` | — | Field path (single-condition shorthand) | | `operator` | `"eq"` | Operator for single-condition shorthand | | `value` | — | Value to compare (not needed for `exists`) | | `conditions` | — | Array of `{field, operator, value}` objects | | `enabled` | `true` | Set to `false` to disable | Non-JSON messages pass through unchanged — conditions only evaluate against JSON fields. # Transforms Source: https://docs.tell.rs/pipeline/transforms/overview Modify data in-flight — filter, redact PII, reduce duplicates, and extract log patterns. Transforms modify events and logs as they flow through the pipeline. You can drop unwanted data, scrub PII, consolidate duplicates, and extract patterns from logs — all before data reaches your sinks. Transforms are chained in order within a routing rule. Each one receives a batch, modifies it, and passes it to the next. ## Available transforms | Transform | What it does | Use when… | | - | - | - | | [Filter](/pipeline/transforms/filter) | Drop or keep events by condition | You want to exclude health checks, debug logs, or internal traffic | | [Redact](/pipeline/transforms/redact) | Remove or pseudonymize PII | You need GDPR compliance or want to scrub emails, IPs, phone numbers | | [Reduce](/pipeline/transforms/reduce) | Consolidate duplicate events | Error storms or repeated events are inflating your volume | | [Pattern](/pipeline/transforms/pattern) | Extract log patterns with Drain | You want to cluster similar log messages for anomaly detection | ## Configuration Add transforms to any routing rule in your `tell.toml`: ```toml theme={null} [[routing.rules]] match = { source = "app" } sinks = ["analytics"] [[routing.rules.transformers]] type = "filter" action = "drop" field = "path" operator = "eq" value = "/health" [[routing.rules.transformers]] type = "redact" strategy = "hash" hash_key = "your-secret-key" patterns = ["email", "ipv4"] scan_all = true ``` Transforms run in the order listed. In this example, health check events are dropped first, then PII is hashed in the remaining events. ## What's next * [Filter](/pipeline/transforms/filter) — drop or keep events by condition * [Redact](/pipeline/transforms/redact) — scrub PII with 11 built-in patterns * [Reduce](/pipeline/transforms/reduce) — consolidate duplicate events * [Pattern](/pipeline/transforms/pattern) — extract log patterns * [Routing](/pipeline/routing) — how transforms fit into routing rules # Pattern Extraction Source: https://docs.tell.rs/pipeline/transforms/pattern Cluster log messages into patterns using the Drain algorithm — powers anomaly detection. The pattern transform extracts recurring patterns from log messages using the Drain algorithm. A message like `"User alice logged in from 10.0.0.1"` becomes the pattern `"User <*> logged in from <*>"` — grouping thousands of similar messages into a handful of templates. Patterns are used by Tell's [anomaly detection](/analytics/predictions/anomaly) to spot unusual log activity. ## Quick start ```toml theme={null} [[routing.rules.transformers]] type = "pattern_matcher" ``` That's it. The defaults work well for most log volumes. Each log message gets a pattern ID attached, enabling pattern-based grouping and anomaly scoring downstream. ## How it works The Drain algorithm builds a tree of log patterns: 1. Incoming messages are tokenized (split on whitespace) 2. Tokens that look like variables — numbers, IPs, UUIDs, URLs, timestamps, paths, emails — are detected automatically 3. Messages are matched against existing patterns by similarity 4. If a match is found, the pattern's count increments. If not, a new pattern is created. The result is a set of templates like: ``` "User <*> logged in from <*>" count: 14,203 "Payment <*> failed with code <*>" count: 847 "Request to <*> timed out after <*> ms" count: 312 ``` ## Similarity threshold The `similarity_threshold` controls how aggressively messages are clustered: ```toml theme={null} [[routing.rules.transformers]] type = "pattern_matcher" similarity_threshold = 0.5 ``` | Value | Effect | | - | - | | Lower (0.3) | More lenient — fewer patterns, broader clusters | | Default (0.5) | Balanced clustering | | Higher (0.7) | More strict — more patterns, tighter clusters | ## Persistence By default, patterns live in memory and are lost on restart. Enable file persistence to save them: ```toml theme={null} [[routing.rules.transformers]] type = "pattern_matcher" persistence_enabled = true persistence_file = "/var/lib/tell/patterns.json" ``` Patterns are saved in the background — persistence doesn't slow down the transform pipeline. ## Caching The pattern matcher uses a 3-level cache for performance: | Level | What it caches | Typical hit rate | | - | - | - | | L1 | Exact message hash | 70-80% | | L2 | Normalized template hash | Catches similar messages | | L3 | Drain tree lookup | Fallback for new messages | Most messages hit L1 (identical to a recent message) and skip the tree entirely. The `cache_size` setting controls L1 capacity. ## Reference | Field | Default | Description | | - | - | - | | `type` | — | `"pattern_matcher"` | | `similarity_threshold` | `0.5` | How similar messages must be to share a pattern (0.0–1.0) | | `max_child_nodes` | `100` | Maximum branches per tree node | | `cache_size` | `100000` | L1 cache capacity (exact message hashes) | | `persistence_enabled` | `false` | Save patterns to disk | | `persistence_file` | — | File path for pattern storage | | `enabled` | `true` | Set to `false` to disable | ## What's next * [Anomaly Detection](/analytics/predictions/anomaly) — how patterns power log anomaly scoring * [Logs](/tracking/logs) — structured logging reference # Redact Transform Source: https://docs.tell.rs/pipeline/transforms/redact Scrub PII from events and logs — 11 built-in patterns, custom regex, hash or mask. The redact transform removes or pseudonymizes personally identifiable information before data leaves your pipeline. It ships with 11 built-in patterns for common PII types and supports custom regex for anything else. ## Quick start Redact all emails and IP addresses: ```toml theme={null} [[routing.rules.transformers]] type = "redact" patterns = ["email", "ipv4"] scan_all = true ``` This replaces matches with `[REDACTED]`. To keep data linkable across events, use hashing instead: ```toml theme={null} [[routing.rules.transformers]] type = "redact" strategy = "hash" hash_key = "your-secret-key" patterns = ["email", "ipv4"] scan_all = true ``` Hashing produces deterministic pseudonyms like `usr_7kJ9mNpQ3xYz` — the same input always produces the same hash, so you can still correlate events without storing raw PII. ## Built-in patterns | Pattern | Detects | Hash prefix | | - | - | - | | `email` | Email addresses | `usr_` | | `phone` | Phone numbers (international formats) | `phn_` | | `credit_card` | Credit card numbers (13-19 digits) | `cc_` | | `ssn_us` | US Social Security Numbers | `ssn_` | | `cpr_dk` | Danish CPR numbers | `cpr_` | | `nino_uk` | UK National Insurance Numbers | `nin_` | | `bsn_nl` | Dutch BSN numbers | `bsn_` | | `ipv4` | IPv4 addresses | `ip4_` | | `ipv6` | IPv6 addresses | `ip6_` | | `passport` | Passport numbers | `pas_` | | `iban` | International Bank Account Numbers | `iba_` | ## Strategies | Strategy | Output | Use when… | | - | - | - | | `redact` (default) | `[REDACTED]` | You want PII completely removed | | `hash` | `usr_7kJ9mNpQ3xYz` | You need to correlate events without raw PII | Hashing uses HMAC-SHA256 with your `hash_key`. Different keys produce different hashes, so each workspace can be isolated. ## Targeted fields Instead of scanning all strings (`scan_all = true`), you can target specific JSON paths for better performance: ```toml theme={null} [[routing.rules.transformers]] type = "redact" strategy = "hash" hash_key = "your-secret-key" [[routing.rules.transformers.fields]] path = "user.email" pattern = "email" [[routing.rules.transformers.fields]] path = "client_ip" pattern = "ipv4" strategy = "redact" ``` Each targeted field can override the default strategy. In this example, emails are hashed but IP addresses are fully redacted. ## Custom patterns Define your own regex patterns for domain-specific PII: ```toml theme={null} [[routing.rules.transformers]] type = "redact" strategy = "hash" hash_key = "your-secret-key" scan_all = true [[routing.rules.transformers.custom_patterns]] name = "employee_id" regex = "EMP-\\d{6}" prefix = "emp_" [[routing.rules.transformers.custom_patterns]] name = "order_id" regex = "ORD-[0-9A-Z]{8}" prefix = "ord_" ``` Custom patterns work with both strategies and are checked alongside built-in patterns. ## Reference | Field | Default | Description | | - | - | - | | `type` | — | `"redact"` | | `strategy` | `"redact"` | `"redact"` or `"hash"` | | `hash_key` | — | Secret key for HMAC-SHA256 (required if `strategy = "hash"`) | | `patterns` | `[]` | Built-in pattern names to detect | | `fields` | `[]` | Targeted field objects: `{path, pattern, strategy?}` | | `custom_patterns` | `[]` | Custom regex objects: `{name, regex, prefix}` | | `scan_all` | `false` | Scan all string values (slower but comprehensive) | | `enabled` | `true` | Set to `false` to disable | At least one of `patterns`, `fields`, or `custom_patterns` is required. # Reduce Transform Source: https://docs.tell.rs/pipeline/transforms/reduce Consolidate duplicate events into single events with count metadata. The reduce transform groups similar events and flushes them as a single event with a count. When an error storm sends 10,000 identical messages in a minute, reduce collapses them into one event with `_reduced_count: 10000` — cutting storage costs without losing information. ## Quick start ```toml theme={null} [[routing.rules.transformers]] type = "reduce" group_by = ["error_code"] window_ms = 5000 ``` This groups events by `error_code` and flushes each group after 5 seconds. ## How it works 1. Events are grouped by the fields you specify in `group_by` 2. When a group reaches `max_events` or the time window expires, it flushes 3. The first event in each group is emitted with metadata fields added: * `_reduced_count` — how many events were consolidated * `_reduced_span_ms` — time span from first to last event in the group Groups with fewer than `min_events` pass through unchanged — no metadata is added. ## Grouping The `group_by` field controls what counts as "similar." Events with the same values for these fields land in the same group. ```toml theme={null} # Group by a single field group_by = ["error_code"] # Group by multiple fields group_by = ["error_code", "host", "severity"] # Nested JSON paths work too group_by = ["error.code", "request.user_id"] ``` If `group_by` is empty, events are grouped by their entire content — only exact duplicates are consolidated. ## Examples **Collapse error storms:** ```toml theme={null} [[routing.rules.transformers]] type = "reduce" group_by = ["error_code", "service"] window_ms = 10000 max_events = 500 min_events = 5 ``` Only groups with 5+ events are reduced. Smaller groups pass through unchanged. **Deduplicate health checks:** ```toml theme={null} [[routing.rules.transformers]] type = "reduce" group_by = ["path"] window_ms = 60000 min_events = 1 ``` **Aggressive cost reduction:** ```toml theme={null} [[routing.rules.transformers]] type = "reduce" group_by = ["error_type"] window_ms = 1000 max_events = 50 min_events = 1 ``` ## Reference | Field | Default | Description | | - | - | - | | `type` | — | `"reduce"` | | `group_by` | `[]` | Fields to group by (empty = exact content match) | | `window_ms` | `5000` | Time window in milliseconds before flushing a group | | `max_events` | `1000` | Maximum events per group before forced flush | | `min_events` | `2` | Minimum events to apply reduction (below this, events pass through unchanged) | | `enabled` | `true` | Set to `false` to disable | # API Keys Source: https://docs.tell.rs/running-tell/api-keys Streaming keys for data ingestion and programmatic keys for the HTTP API. Tell has two types of keys. **Streaming keys** authenticate SDKs and pipeline sources sending data over TCP, HTTP, or syslog. **Programmatic API keys** authenticate HTTP API calls for automation and integrations. ## Streaming keys Streaming keys are 32-character hex strings that map directly to a workspace: ``` a1b2c3d4e5f60718293a4b5c6d7e8f90 ``` You get your first streaming key when you run [setup](/api/authentication). SDKs and pipeline sources use this key to authenticate data ingestion. ### Managing streaming keys Use the CLI to create, list, and delete streaming keys: ```bash theme={null} # List keys tell apikeys # Create a new key for workspace 1 tell apikeys new mobile-prod --workspace 1 # Show the full key tell apikeys show mobile-prod # Delete a key tell apikeys delete mobile-prod ``` ### Key file format Streaming keys are stored in a key file with one key per line: ``` # Production workspace a1b2c3d4e5f60718293a4b5c6d7e8f90:1 # Staging workspace b2c3d4e5f6071829304a5b6c7d8e9f01:2:staging ``` Format: `{32_hex_chars}:{workspace_id}[:{optional_name}]`. Lines starting with `#` are comments. ### Using streaming keys Configure your SDK with the key: ```javascript theme={null} import { Tell } from '@tell-rs/node'; const tell = new Tell('a1b2c3d4e5f60718293a4b5c6d7e8f90', { endpoint: 'https://your-tell-server', }); ``` Pipeline sources (TCP, HTTP, syslog) validate the key on every connection. ## Programmatic API keys For automating HTTP API calls (querying metrics, managing boards, etc.), create programmatic API keys: ```bash theme={null} curl -X POST https://your-tell-server/api/v1/user/apikeys \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "CI Pipeline", "workspace_id": "ws-1", "expires_at": "2026-12-31T23:59:59Z" }' ``` The response includes the full key — it's only shown once: ```json theme={null} { "key": "tell_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", "id": "key_...", "name": "CI Pipeline", "key_prefix": "tell_a1b2" } ``` Use it like a regular JWT token: ```bash theme={null} curl https://your-tell-server/api/v1/metrics/dau?range=30d \ -H "Authorization: Bearer tell_a1b2c3d4..." ``` ### Key options | Field | Required | Description | | - | - | - | | `name` | Yes | Display name (1-100 characters) | | `description` | No | Optional description (max 500 characters) | | `workspace_id` | No | Scope to a workspace (empty = user-level key) | | `permissions` | No | Specific permissions (empty = inherit from your role) | | `expires_at` | No | Expiration date (max 1 year from now, never expires if omitted) | ### Managing API keys Both list endpoints return a [list response](/api/authentication#list-responses) with keys in `items` and the number of keys in `total`. ```bash theme={null} # List your keys curl https://your-tell-server/api/v1/user/apikeys \ -H "Authorization: Bearer $TOKEN" # Revoke a key curl -X DELETE https://your-tell-server/api/v1/user/apikeys/{key-id} \ -H "Authorization: Bearer $TOKEN" ``` Admins can view and revoke any key in their workspace: ```bash theme={null} # List all keys in workspace curl https://your-tell-server/api/v1/admin/apikeys \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws-1" # Revoke any key in workspace curl -X DELETE https://your-tell-server/api/v1/admin/apikeys/{key-id} \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws-1" ``` ## Security * Streaming keys use constant-time comparison to prevent timing attacks * Programmatic API keys are stored as SHA-256 hashes — only the `key_prefix` is visible after creation * Programmatic API keys inherit the creator's role — they can't do more than you can * Passwords are hashed with Argon2id * Revoked tokens are blacklisted and cannot be reused even if the JWT hasn't expired * All key creation and revocation events are [audit logged](/running-tell/audit-privacy) # Audit & Privacy Source: https://docs.tell.rs/running-tell/audit-privacy Audit logging, GDPR/CCPA compliance, data anonymization, PII redaction. Tell is designed to be privacy-friendly out of the box. It collects no personal information by default, uses no cookies, and gives you full control over your data because it runs on your infrastructure. ## Privacy by default **No cookies.** Tell identifies visitors with random device UUIDs generated by the SDK. No cookie banner needed. **No PII collected.** Events and sessions use anonymous device IDs — not emails, names, or IP addresses. You choose what properties to send. **Self-hosted.** Your data stays on your servers. Nothing is sent to third parties. This makes Tell compliant with GDPR and CCPA without additional configuration. ## PII redaction If your events or logs contain personal information (emails in log messages, IPs in error traces), the redact transform scrubs them before data reaches storage. Tell includes 11 built-in PII patterns: | Pattern | Matches | | - | - | | `email` | Email addresses | | `phone` | Phone numbers (E.164 and common formats) | | `credit_card` | 13-19 digit card numbers | | `ssn_us` | US Social Security Numbers | | `cpr_dk` | Danish CPR numbers | | `nino_uk` | UK National Insurance Numbers | | `bsn_nl` | Dutch BSN numbers | | `ipv4` | IPv4 addresses | | `ipv6` | IPv6 addresses | | `passport` | Passport numbers | | `iban` | International Bank Account Numbers | **Redact mode** replaces matches with `[REDACTED]`. **Hash mode** replaces them with a deterministic pseudonym (e.g., `usr_7Hx9KmPqR2sT`) so you can still correlate records without exposing the original value. ```toml theme={null} [[routing.rules.transformers]] type = "redact" strategy = "hash" hash_key = "your-workspace-secret" patterns = ["email", "phone", "ipv4"] ``` For targeted redaction on specific fields (faster than scanning everything): ```toml theme={null} [[routing.rules.transformers]] type = "redact" strategy = "hash" hash_key = "your-workspace-secret" [[routing.rules.transformers.fields]] path = "user.email" pattern = "email" [[routing.rules.transformers.fields]] path = "client_ip" pattern = "ipv4" strategy = "redact" ``` You can also define custom patterns for internal identifiers like employee IDs. See [Transforms](/pipeline/transforms/overview) for the full redact transform reference. ### Client-side redaction For defense in depth, the JavaScript SDKs also provide a `redact()` utility that strips sensitive data *before* it leaves the client. This is complementary to server-side pipeline redaction — use both for layered protection. * [Browser SDK — Redaction & beforeSend](/tracking/sdks/javascript/browser#redaction--beforesend) * [Node.js SDK — Redaction & beforeSend](/tracking/sdks/javascript/node#redaction--beforesend) ## Audit logging Tell records 40 typed audit actions across authentication, data access, workspace management, and admin operations. Every action includes who did it, what they did, whether it succeeded, and from which source (API, CLI, MCP, or public link). ### What's audited | Category | Actions | | - | - | | **Authentication** | Login, logout, failed login, token refresh, API key create/revoke | | **Boards** | Create, update, delete, run, share | | **Sharing** | Create link, revoke link, public access | | **Data queries** | SQL query execution, query denied, query timeout | | **Workspace** | Add/remove member, role change, settings update | | **Invitations** | Create, cancel, accept | | **Saved metrics** | Create, update, delete | | **Segments** | Create, update, delete | | **MCP** | Auth failed, session create | | **Admin** | Config change, setup, user create/update/delete | ### Outcomes Each audit event records an outcome: * **Success** — action completed normally * **Denied** — permission check failed * **Failed** — runtime error * **Blocked** — rate limited or timed out ### Sensitive actions Some actions are classified as sensitive and should always be logged regardless of log level filtering. These include failed logins, API key operations, SQL queries, membership changes, and all admin operations. ### Enabling audit logs Audit logging is enabled on the API server. Events are emitted as structured log entries with target `audit`, so they flow through your existing log infrastructure. Filter for `target: "audit"` to route them to a SIEM, log aggregator, or dedicated file. ## Product telemetry Tell collects anonymous usage telemetry to understand how the product is used. It's enabled by default and designed to be transparent — you can inspect exactly what's sent before deciding to keep it on. **No PII.** The install ID is a SHA256 hash derived from your hostname. It can't be reversed to identify you. No IP addresses, hostnames, or user data are included. ### What's collected | Category | Data | | - | - | | **Deployment** | Anonymous install ID, Tell version, OS, architecture, CPU cores, memory | | **Configuration** | Which source/sink/integration/transformer types are enabled (names only, not values) | | **Runtime** | Messages processed, bytes processed, uptime, error count, active connections | | **Feature usage** | Which features have been used: tail, query, TUI, boards count, API keys count, workspaces count | Telemetry is sent once per week via TCP to `t.tell.rs:50000`. ### Inspect the payload See exactly what would be sent: ```bash theme={null} tell telemetry show tell telemetry show --json ``` ### Opt out ```toml theme={null} [telemetry] enabled = false ``` ### Configuration | Field | Default | Description | | - | - | - | | `enabled` | `true` | Set `false` to disable all telemetry | | `interval` | `"7d"` | Reporting interval (`"1d"`, `"7d"`, `"30d"`) | ## Rate limiting Tell rate-limits sensitive endpoints to prevent abuse: | Endpoint type | Limit | | - | - | | Auth (login, token) | 10 req/min | | Data queries (SQL) | 30 req/min | | Public shares | 60 req/min | # ClickHouse Setup Source: https://docs.tell.rs/running-tell/clickhouse Set up ClickHouse as Tell's analytics storage backend. Tell uses ClickHouse for analytics storage — events, logs, sessions, and query results all live here. You need a running ClickHouse instance before Tell can store data. ## Pick a provider Run ClickHouse on your own infrastructure. Managed service from the ClickHouse team. Managed ClickHouse with a real-time data platform. ## Self-hosted ```bash theme={null} curl https://clickhouse.com/ | sh ``` ```bash theme={null} ./clickhouse server ``` ```bash theme={null} tell clickhouse init myproject ``` This creates: * A database and all required tables * A ClickHouse user with the right permissions * A config file at `configs/myproject.toml` with generated credentials ```bash theme={null} tell run -c configs/myproject.toml ``` ## Using a managed provider For ClickHouse Cloud or Tinybird, you'll need the connection details from your provider. Pass them to the init command: ```bash theme={null} tell clickhouse init myproject --host your-instance.clickhouse.cloud --port 9440 --secure ``` The generated config will include your connection details. # Server Configuration Source: https://docs.tell.rs/running-tell/configuration TOML configuration reference for Tell server, API, auth, and query backend. Tell server configuration lives in a single TOML file. This page covers the server-side settings — API server, authentication, query backend, logging, and integrations. For pipeline configuration (sources, sinks, routing), see [Pipeline Configuration](/pipeline/configuration). ## Config resolution Tell looks for a config file in this order: 1. `--config` flag — `tell run --config /path/to/config.toml` 2. `TELL_CONFIG` environment variable — `export TELL_CONFIG=/etc/tell/config.toml` 3. `tell-config.toml` in the current directory 4. `config.toml` in the current directory On servers, set `TELL_CONFIG` so all commands use the right config without flags. See [Self-hosting](/running-tell/self-hosting) for setup. ## Minimal server config A Tell server with local auth and ClickHouse query backend: ```toml theme={null} [api_server] port = 3000 [auth] provider = "local" jwt_secret = "your-secret-key-at-least-32-characters-long" [query] clickhouse_url = "http://localhost:8123" clickhouse_database = "tell" ``` ## API server ```toml theme={null} [api_server] enabled = true host = "::" port = 3000 audit_logging = false control_db = "~/.tell/control.db" ``` | Field | Default | Notes | | - | - | - | | `port` | 3000 | HTTP port | | `host` | `"::"` | Bind address (dual-stack IPv4+IPv6) | | `audit_logging` | false | Log all HTTP requests | | `control_db` | `~/.tell/control.db` | SQLite database for workspaces, boards, users | | `tls_cert_path` | — | TLS certificate PEM path | | `tls_key_path` | — | TLS private key PEM path | ## Authentication Tell supports local auth (built-in) and WorkOS (SSO). ### Local auth ```toml theme={null} [auth] provider = "local" jwt_secret = "your-secret-key-at-least-32-characters-long" jwt_expires_in = "24h" [auth.local] db_path = "data/users.db" allow_registration = false ``` * `jwt_secret` — required, must be at least 32 characters * `allow_registration` — set to `true` to let users self-register after initial setup * `jwt_expires_in` — how long tokens last (default: 24 hours) ### WorkOS (SSO) ```toml theme={null} [auth] provider = "workos" [auth.workos] api_key = "sk_live_..." client_id = "client_..." redirect_uri = "https://your-domain.com/auth/callback" ``` Both `api_key` and `client_id` are required when using WorkOS. ## Query backend Tell auto-detects the query backend. If a ClickHouse sink is configured, it uses ClickHouse. Otherwise, it falls back to the local Polars engine. ### ClickHouse (recommended) ```toml theme={null} [query] clickhouse_url = "http://localhost:8123" clickhouse_database = "tell" clickhouse_username = "default" clickhouse_password = "" ``` Or reference an existing sink by name: ```toml theme={null} [query] clickhouse_sink = "clickhouse" ``` ### Local (Polars) ```toml theme={null} [query] backend = "polars" data_dir = "./data" ``` Reads Arrow IPC files from disk. Good for development and small deployments. ## Logging ```toml theme={null} [log] level = "info" format = "console" output = "stdout" ``` | Field | Default | Options | | - | - | - | | `level` | `"info"` | `trace`, `debug`, `info`, `warn`, `error` | | `format` | `"console"` | `"console"` (human-readable), `"json"` (structured) | | `output` | `"stdout"` | `"stdout"`, `"stderr"`, or a file path | ## MCP server ```toml theme={null} [mcp] http_enabled = true stdio_enabled = true ``` HTTP transport runs on the API server at `/api/v1/mcp`. Stdio transport is available via `tell mcp`. See [MCP](/tools/mcp) for details. ## LLM integration ```toml theme={null} [llm] enabled = true provider = "anthropic" model = "claude-sonnet-4-5-20250929" ``` | Field | Default | Notes | | - | - | - | | `enabled` | false | Enable AI assistant features | | `provider` | `"anthropic"` | `"anthropic"` or `"openai-compatible"` | | `model` | `"claude-sonnet-4-5-20250929"` | Model ID | | `api_key` | — | API key (env variable takes priority) | | `base_url` | — | Custom URL for OpenAI-compatible providers | ## API client Settings for the `tell` CLI when connecting to a remote server: ```toml theme={null} [api] url = "http://localhost:3000" credential_store = "file" ``` | Field | Default | Options | | - | - | - | | `url` | `http://localhost:3000` | Server URL | | `credential_store` | `"file"` | `"file"`, `"keyring"`, or `"auto"` | ## Metrics reporting ```toml theme={null} [metrics] enabled = true interval = "1h" format = "human" ``` Controls pipeline metrics output — batches processed, bytes written, connection counts. Set `format = "json"` for machine-readable output. ## Integrations ```toml theme={null} [[plugins]] name = "github" entity = "all" config.token = "${GITHUB_TOKEN}" ``` Each integration is configured as a `[[plugins]]` entry with a name, entity, and service-specific config fields. See [Integrations](/tracking/integrations/overview) for details. # Roles & Permissions Source: https://docs.tell.rs/running-tell/roles Four hierarchical roles — viewer, editor, admin, platform — that control what users can do. Tell uses a simple role-based access control (RBAC) system. Every user has a role in each workspace they belong to. Roles are hierarchical — higher roles inherit all permissions from lower ones. ## Roles | Role | What they can do | | - | - | | **Viewer** | View analytics, dashboards, and shared content | | **Editor** | Everything a Viewer can, plus create and edit their own boards, saved metrics, and queries | | **Admin** | Everything an Editor can, plus manage workspace members, settings, invite users, run raw SQL, and edit anyone's content | | **Platform** | Everything an Admin can, plus cross-workspace operations (for self-hosted and enterprise deployments) | ## Permissions Roles map to four explicit permissions. View is implicit for all workspace members. | Permission | Minimum role | What it controls | | - | - | - | | `create` | Editor | Create/edit/share own content (boards, saved metrics) | | `raw_query` | Admin | Execute raw SQL queries against the analytics database | | `admin` | Admin | Manage workspace (members, settings, integrations) | | `platform` | Platform | Cross-workspace operations | ## Common scenarios **Your analyst needs to build dashboards.** Give them the Editor role. They can create boards and saved metrics, but can't manage team members or run arbitrary SQL. **A developer needs raw SQL access.** They need the Admin role. Editors can use built-in metrics and boards, but raw SQL requires Admin. **Your DevOps team manages the Tell deployment.** Give them the Platform role on self-hosted installations. This allows cross-workspace operations like managing all workspaces and global settings. ## Assigning roles Roles are assigned when inviting users to a workspace: ```bash theme={null} # Invite as viewer (default) curl -X POST https://your-tell-server/api/v1/admin/workspace/invites \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"email": "analyst@example.com", "workspace_id": "ws-1", "role": "editor"}' ``` Invitations expire after 7 days. The invited user must log in and accept the invite. Their email must match the invite. ## Workspace-scoped roles A user can have different roles in different workspaces. For example, you might be an Admin in your team's workspace but a Viewer in a shared analytics workspace. The role in the JWT token reflects the user's global role. Workspace-specific roles are checked separately when accessing workspace resources. ## The first user The first user created during [setup](/api/authentication) automatically gets the Platform role — the highest privilege level. This user can then invite others and assign appropriate roles. # Self-hosting Source: https://docs.tell.rs/running-tell/self-hosting Deploy Tell on your own Linux server with systemd. Run Tell on your own infrastructure. One binary, no containers. Data never leaves your servers. ```bash theme={null} curl -sSfL https://tell.rs | bash cp ~/.local/bin/tell /usr/local/bin/tell ``` Generate a config file and move it to a dedicated location: ```bash theme={null} tell init mkdir -p /etc/tell mv tell-config.toml /etc/tell/config.toml ``` Edit `/etc/tell/config.toml` — set `data_dir` to `/opt/tell` and configure your pipeline: ```toml theme={null} data_dir = "/opt/tell" ``` Tell uses ClickHouse as the default analytics backend. Install it, start the server, and create the Tell tables: ```bash theme={null} curl https://clickhouse.com/ | sh ./clickhouse server & ``` `tell init` generated a `setup.sql` file alongside the config. Run it to create the database and tables: ```bash theme={null} clickhouse-client < setup.sql ``` This assumes ClickHouse is running with default settings. See [clickhouse.com](https://clickhouse.com/docs) for installation and configuration details. ```bash theme={null} useradd -r -s /bin/false tell mkdir -p /opt/tell chown tell:tell /opt/tell chown tell:tell /etc/tell/config.toml chmod 600 /etc/tell/config.toml ``` ```bash theme={null} echo '[Unit] Description=Tell After=network.target [Service] Type=simple User=tell Group=tell Environment=TELL_CONFIG=/etc/tell/config.toml ExecStart=/usr/local/bin/tell run Restart=always RestartSec=5 WorkingDirectory=/opt/tell NoNewPrivileges=yes PrivateTmp=yes ProtectSystem=strict ProtectHome=yes ReadWritePaths=/opt/tell [Install] WantedBy=multi-user.target' > /etc/systemd/system/tell.service ``` ```bash theme={null} systemctl daemon-reload systemctl enable --now tell systemctl status tell ``` Tell is running. By default, HTTP ingestion listens on port **8080** and TCP on **50000**. Point your SDKs to `your-server:8080`, or put Caddy in front to serve on 443. Tell is installed and running. To start collecting data, head to the [Quickstart](/getting-started/quickstart). Set `TELL_CONFIG` in your shell so CLI commands (`tell plugin`, `tell tail`, `tell query`, etc.) pick up the right config and data directory. ```bash Bash theme={null} echo 'export TELL_CONFIG=/etc/tell/config.toml' >> ~/.bashrc source ~/.bashrc ``` ```bash Fish theme={null} set -Ux TELL_CONFIG /etc/tell/config.toml ``` ```bash Zsh theme={null} echo 'export TELL_CONFIG=/etc/tell/config.toml' >> ~/.zshrc source ~/.zshrc ``` ```bash theme={null} curl -sSfL https://tell.rs | bash cp ~/.local/bin/tell /usr/local/bin/tell systemctl restart tell ``` *** ## Optional: TLS with Caddy If you're exposing the Tell API or dashboard publicly, put a reverse proxy in front of it: ```bash theme={null} apt install -y caddy ``` ```bash theme={null} echo 'tell.example.com { reverse_proxy localhost:3000 }' > /etc/caddy/Caddyfile systemctl restart caddy ``` Caddy handles TLS certificates automatically via Let's Encrypt. Point your DNS A record to the server IP. # Workspaces & Teams Source: https://docs.tell.rs/running-tell/workspaces Multi-tenant workspaces with team member management and data isolation. Every Tell deployment is organized into workspaces. Each workspace is a fully isolated environment — its own database, API keys, team members, and analytics data. Nothing leaks between workspaces. ## How workspaces work When you run Tell for the first time, [setup](/api/authentication) creates your first workspace and admin account. From there, you can create additional workspaces for each app, product, or environment you want to track separately. The most common pattern is **one workspace per app** — like projects on Vercel. If you ship a web app, a mobile app, and an internal tool, each gets its own workspace with its own data, API keys, and team access. * **my-saas-app** — product analytics and logs for your main product * **mobile-app** — iOS and Android event tracking * **marketing-site** — page views and conversion tracking * **internal-tools** — operational dashboards You can also use workspaces to separate environments (production vs staging) or departments — whatever boundary makes sense for your setup. Each workspace gets its own ClickHouse database and its own set of streaming API keys. ## Creating a workspace ```bash theme={null} curl -X POST https://your-tell-server/api/v1/user/workspaces \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "Staging", "slug": "staging"}' ``` The slug is a URL-friendly identifier (alphanumeric and hyphens, 1-50 characters). You automatically become the admin of any workspace you create. After creating a workspace, run `tell setup-db init` to create its ClickHouse database and tables: ```bash theme={null} tell setup-db init --workspace staging ``` ## Team members ### Inviting users Admins invite users by email with a specific role. Invitations expire after 7 days. ```bash theme={null} curl -X POST https://your-tell-server/api/v1/admin/workspace/invites \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"email": "analyst@example.com", "workspace_id": "ws_abc123", "role": "editor"}' ``` The invited user receives a link to accept the invitation. If they don't have a Tell account yet, one is created when they accept. ### Roles Each team member has a role that controls what they can do: | Role | Purpose | | - | - | | **Viewer** | View dashboards and analytics | | **Editor** | Create and edit boards, saved metrics, and queries | | **Admin** | Manage members, settings, and all content | | **Platform** | Cross-workspace operations (self-hosted deployments) | A user can have different roles in different workspaces. See [Roles & Permissions](/running-tell/roles) for details. ### Managing members ```bash theme={null} # List workspace users curl "https://your-tell-server/api/v1/admin/users?limit=50" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" # Change a user's role curl -X PUT https://your-tell-server/api/v1/admin/users/{user-id} \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" \ -H "Content-Type: application/json" \ -d '{"role": "admin"}' # Remove a user curl -X DELETE https://your-tell-server/api/v1/admin/users/{user-id} \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-ID: ws_abc123" ``` You can't assign a role higher than your own — an Admin can't make someone a Platform user. ## Workspace isolation Workspaces are isolated at every level: * **Database** — each workspace gets a separate ClickHouse database * **API keys** — streaming keys and programmatic keys are scoped to a workspace * **Queries** — SQL queries are automatically scoped to the workspace's database * **Boards and metrics** — all content belongs to a workspace * **Audit logs** — [audit events](/running-tell/audit-privacy) are workspace-scoped ## Platform management Users with the Platform role (typically your DevOps team) can manage workspaces across the entire deployment: ```bash theme={null} # List all workspaces curl https://your-tell-server/api/v1/admin/workspaces \ -H "Authorization: Bearer $TOKEN" # Delete a workspace curl -X DELETE https://your-tell-server/api/v1/admin/workspaces/{id} \ -H "Authorization: Bearer $TOKEN" ``` Deleting a workspace permanently removes all its data, members, and API keys. ## What's next * [Roles & Permissions](/running-tell/roles) — detailed role and permission reference * [API Keys](/running-tell/api-keys) — managing streaming and programmatic keys * [Workspaces API](/api/workspaces) — full HTTP API reference # Ask Source: https://docs.tell.rs/tools/ask Query an LLM about your data from the command line. Query an LLM about your data from the command line with `tell ask`. # Generative UI Source: https://docs.tell.rs/tools/blocks Tell Block Format — streaming JSONL for LLM-generated dashboards. When you ask Tell a question with `tell ask` or through [MCP](/tools/mcp), the response comes back as **blocks** — a streaming JSONL format that renders as rich UI on any platform. The same JSON produces charts in your terminal, on the web dashboard, and in native apps. ## How it works An LLM generates one JSON object per line. Each object has a `t` field that identifies its type. Your client reads lines as they arrive and renders each block immediately — no waiting for the full response. ```jsonl theme={null} {"t":"text","c":"Here are your metrics:"} {"t":"metric","title":"DAU","value":12847,"unit":"users","change":15.2} {"t":"chart","v":"sparkline","data":[[0,10],[1,15],[2,12]]} ``` This renders as a text paragraph, a large number with a trend arrow, and a sparkline chart — all streaming in real time. ## Block types Tell supports 8 block types: | Type | What it shows | | - | - | | `text` | Plain text paragraph | | `metric` | Single KPI — value, unit, trend, color | | `metrics` | 2-4 KPIs in a row | | `chart` | Line, bar, area, pie, or sparkline | | `table` | Rows and columns with headers | | `status` | Health indicator (healthy, degraded, down, unknown) | | `callout` | Alert box (info, warning, error, success) | | `code` | Source code with syntax highlighting | ### metric A single number with optional context: ```json theme={null} {"t":"metric","title":"Error Rate","value":0.12,"unit":"%","change":-5.2,"color":"success"} ``` * `title` — what the number measures * `value` — the number itself * `unit` — display unit (users, %, ms, etc.) * `change` — percentage trend (positive = up, negative = down) * `color` — hint: `success`, `error`, `warning`, `info`, or `default` ### metrics Multiple KPIs side by side: ```json theme={null} {"t":"metrics","items":[ {"title":"DAU","value":12847,"unit":"users"}, {"title":"WAU","value":45230,"unit":"users"}, {"title":"MAU","value":128450,"unit":"users"} ]} ``` ### chart Data visualization with five chart variants: ```json theme={null} {"t":"chart","v":"line","title":"User Growth","data":[[1704067200000,10000],[1704153600000,10500],[1704240000000,11200]]} ``` * `v` — chart variant: `line`, `bar`, `area`, `pie`, or `sparkline` * `data` — array of `[x, y]` pairs (x is typically a timestamp in milliseconds) In the TUI, sparklines render as compact Unicode bars (`▂▃▅▆▇`). ### table Structured data with headers: ```json theme={null} {"t":"table","cols":["Service","Requests","P99"],"rows":[["api",52000,"45ms"],["auth",31000,"23ms"]]} ``` Cell values can be strings or numbers. ### status Health indicators for systems or components: ```json theme={null} {"t":"status","label":"API Gateway","state":"healthy","message":"All operational"} ``` States: `healthy` (green), `degraded` (yellow), `down` (red), `unknown` (gray). ### callout Highlighted notices: ```json theme={null} {"t":"callout","v":"warning","title":"Rate Limit","c":"You're at 80% quota"} ``` Variants: `info`, `warning`, `error`, `success`. ### code Source code with optional syntax highlighting: ```json theme={null} {"t":"code","lang":"sql","c":"SELECT event_name, COUNT(*) FROM events_v1 GROUP BY event_name"} ``` ## Streaming Blocks stream as JSONL — one complete JSON object per line. The parser buffers partial data and emits blocks as each line completes. If a line can't be parsed, it's reported as an error without crashing the stream. This means LLM output renders progressively as it's generated. ## Where blocks appear * **`tell ask`** — CLI responses rendered in the terminal * **MCP tools** — AI assistant responses in editors and chat interfaces * **TUI** — interactive dashboard views * **Web dashboard** — rendered as React components # CLI Commands Source: https://docs.tell.rs/tools/commands Complete reference for all Tell CLI commands — server, metrics, tail, query, and more. Tell's CLI covers server management, analytics queries, live streaming, AI tools, and setup. Run `tell --help` for details on any command. ## Global flags These flags work with any command: | Flag | Description | | - | - | | `--config ` | Path to config file | | `--data-dir ` | Root data directory (overrides config and `TELL_DATA_DIR` env) | | `--log-level ` | Log level: trace, debug, info, warn, error | ## Server ### `tell run` Start the Tell server. Loads config, binds sources, creates sinks, and runs until interrupted. ```bash theme={null} tell run tell run --config configs/prod.toml ``` With no config file, starts with defaults: TCP source on port 50000, stdout sink. Checks all ports at startup and reports conflicts. Handles SIGHUP to hot-reload [API keys](/running-tell/api-keys) without restart. ### `tell status` Check server health, uptime, and pipeline metrics. ```bash theme={null} tell status # health check tell status --metrics # detailed pipeline stats tell status --watch # live refresh every second tell status --json # JSON output tell status --endpoint http://10.0.0.5:3000 ``` | Flag | Default | Description | | - | - | - | | `--metrics` | off | Show per-source and per-sink stats | | `--watch` | off | Refresh every second with rate calculations | | `--json` | off | Output as JSON | | `--endpoint` | `http://127.0.0.1:3000` | API server URL | ### `tell init` Generate a starter config file. ```bash theme={null} tell init # writes tell-config.toml tell init --output configs/prod.toml ``` ## Analytics ### `tell metrics` Query DAU, MAU, sessions, stickiness, events, and logs from the command line. Uses the `[query]` config section for the backend (ClickHouse or local Arrow IPC). ```bash theme={null} tell metrics dau --range 30d tell metrics wau --range 30d --breakdown device_type tell metrics mau --range 90d --format json tell metrics sessions --range 30d tell metrics stickiness-daily --range 30d tell metrics events --range 7d --name page_view tell metrics events-top --range 7d --limit 10 tell metrics logs --range 7d --level error tell metrics logs-top --range 7d tell metrics drill-down users --range 7d --limit 100 ``` **Subcommands:** | Subcommand | Description | | - | - | | `dau` | Daily active users | | `wau` | Weekly active users | | `mau` | Monthly active users | | `sessions` | Total session count | | `sessions-unique` | Distinct session IDs | | `stickiness-daily` | DAU/MAU ratio | | `stickiness-weekly` | WAU/MAU ratio | | `events` | Event count over time (filter with `--name`) | | `events-top` | Top events by count (limit with `--limit`) | | `logs` | Log volume over time (filter with `--level`) | | `logs-top` | Top logs by count | | `drill-down` | Raw data: `users`, `events`, `logs`, `sessions` | | `saved` | Manage [saved metrics](/analytics/boards#saved-metrics) — `list`, `get`, `create`, `delete` | **Common flags:** | Flag | Default | Description | | - | - | - | | `--range` | `30d` | Time range: `7d`, `30d`, `90d`, `today`, `ytd`, or `YYYY-MM-DD,YYYY-MM-DD` | | `--granularity` | `daily` | `minute`, `hourly`, `daily`, `weekly`, `monthly` | | `--breakdown` | — | Dimension: `device_type`, `country`, `os`, etc. | | `--compare` | — | `previous` (prior period) or `yoy` (year-over-year) | | `--format` | `table` | `table`, `json`, `csv` | | `--workspace` | `1` | Workspace ID | ### `tell query` Execute SQL queries against ClickHouse or local Arrow IPC files (Polars backend). ```bash theme={null} tell query "SELECT event_name, COUNT(*) FROM events GROUP BY event_name" tell query "SELECT * FROM logs WHERE level = 'error' LIMIT 20" --format json tell query "SELECT * FROM events LIMIT 10" --format csv ``` | Flag | Default | Description | | - | - | - | | `--format` | `table` | `table`, `json`, `csv` | | `--config` | — | Config file path (uses `[query]` section) | The backend is resolved from your config: reference a sink by name (`query.sink = "clickhouse"`) or set inline connection details. Falls back to local Arrow IPC files in `arrow/`. ### `tell boards` Manage [boards](/analytics/boards) from the command line. ```bash theme={null} tell boards # list all boards tell boards list --pinned # pinned boards only tell boards create "Product Overview" tell boards create "Q1 Metrics" -d "Quarterly review dashboard" tell boards get # show board details tell boards delete # delete (with confirmation) tell boards pin # pin to top tell boards unpin tell boards share # create/show public link tell boards unshare # revoke public link ``` Add `--json` for machine-readable output. Add `--workspace` to target a specific workspace. ### `tell mark` Create timeline [marks](/analytics/boards#marks) to annotate charts with deploys, incidents, or launches. ```bash theme={null} tell mark "Shipped v2.1.0" tell mark "Payments down" -c incident --at 2025-06-15T23:47:00Z tell mark "Series A announced" -c news -d "Blog post went live" --source-ref https://blog.example.com tell mark --private "Testing hypothesis" tell mark list # last 90 days tell mark list --last 30d -c release tell mark delete ``` | Flag | Default | Description | | - | - | - | | `--category` | `release` | `release`, `news`, or `incident` | | `--at` | now | ISO 8601 timestamp | | `--description` | — | Description text | | `--source-ref` | — | URL or reference | | `--private` | off | Visible only to you | ### `tell workspace` Manage [workspaces](/running-tell/workspaces) and members. ```bash theme={null} tell workspace # list your workspaces tell workspace create "My App" my-app tell workspace members # list members of current workspace tell workspace invite user@example.com --role editor ``` | Flag | Default | Description | | - | - | - | | `--role` | `viewer` | `viewer`, `editor`, or `admin` (for invites) | ## Live streaming ### `tell tail` Stream live data from a running Tell server. Connects via Unix socket to the tap server. ```bash theme={null} tell tail # all data tell tail --type event # events only tell tail --type log --level error # error logs tell tail --workspace 1 --source tcp_main # filter by source tell tail --event "page_*" --output json # glob filter, JSON output tell tail --match "timeout|error" --output compact tell tail --sample 0.01 --rate-limit 100 # 1% sample, max 100/s tell tail --last 50 # replay last 50 batches ``` **Server-side filters** (applied before data leaves the server): | Flag | Description | | - | - | | `--workspace ` | Filter by workspace (repeatable) | | `--source ` | Filter by source ID (repeatable) | | `--type ` | `event`, `log`, `syslog`, `metric`, `trace` (repeatable) | | `--sample ` | Sample rate 0.0–1.0 (e.g. `0.01` = 1%) | | `--rate-limit ` | Max batches per second | | `--last ` | Replay last N batches on connect | **Client-side content filters** (applied after decoding): | Flag | Description | | - | - | | `--event ` | Event name glob (repeatable) | | `--level ` | Log level: `error`, `warning`, `info`, `debug`, `trace` (repeatable) | | `--match ` | Regex pattern matching any string field | **Output:** | Flag | Default | Description | | - | - | - | | `--output` | `text` | `text`, `json`, `compact`, `raw` | | `--no-color` | off | Disable colored output | | `--verbose` | off | Show debug info | | `--quiet` | off | Suppress connection messages | | `--demo` | — | Print sample output to test colors (no connection) | ## AI tools ### `tell ask` Ask the LLM assistant a question about your analytics data. Streams the response to stdout; tool calls print to stderr. ```bash theme={null} tell ask "why did DAU drop last Tuesday?" tell ask --workspace 2 "which users are most likely to churn?" echo "summarize my boards" | tell ask ``` Requires `[llm] enabled = true` in config and `tell login` for authentication. The question can also be piped via stdin. ### `tell mcp` Start the MCP server over stdio for integration with Claude Desktop, Cursor, Zed, and other MCP clients. ```bash theme={null} tell mcp tell mcp --config configs/prod.toml tell mcp --api-key ``` The API key is resolved from: `--api-key` flag, `TELL_API_KEY` environment variable, or stored auth token (`~/.tell/auth.json`). ### `tell skill` Manage custom LLM skills — markdown files that extend the system prompt. ```bash theme={null} tell skill list # show built-in + custom skills tell skill add path/to/skill.md # add a custom skill tell skill remove my-skill # remove by name ``` Custom skills are stored in `~/.tell/skills/`. ## Setup ### `tell apikeys` Manage TCP API keys for SDK ingestion. ```bash theme={null} tell apikeys # list all keys tell apikeys new mobile-prod # create a new key tell apikeys new backend --workspace 2 tell apikeys show mobile-prod # show full key tell apikeys delete old-key # delete (with confirmation) tell apikeys delete old-key -y # skip confirmation ``` ### `tell login` / `tell logout` Authenticate with the Tell API. Credentials are stored securely. ```bash theme={null} tell login tell logout ``` ### `tell license` Manage your Tell license key. ```bash theme={null} tell license show # current status and tier tell license activate # activate a license tell license deactivate # remove license (reverts to free tier) ``` ### `tell schema` Manage ClickHouse workspace schemas — create databases, tables, and users. ```bash theme={null} tell schema create # create workspace schema tell schema create --dry-run # preview SQL without executing tell schema check # verify workspace is set up tell schema destroy # remove all data (with confirmation) tell schema dump # print schema DDL to stdout ``` ## Plugins ### `tell plugin` Manage WASM [integrations](/tracking/integrations/overview) — install, update, and run plugins. ```bash theme={null} tell plugin list # registry + installed status tell plugin list --installed # locally installed only tell plugin search github # search the registry tell plugin info cloudflare # show plugin details tell plugin install cloudflare # install from registry tell plugin install ./my-plugin.wasm --name custom tell plugin update # update all plugins tell plugin update cloudflare # update specific plugin tell plugin remove cloudflare tell plugin pull cloudflare # pull data from plugin ``` ## Advanced ### `tell test` Send sample events and logs to verify the pipeline is working. ```bash theme={null} tell test # 3 events + 3 logs to localhost tell test --server 192.168.1.100:50000 # custom server tell test --events 10 --logs 5 # custom counts tell test --api-key 00010203040506070809... # custom API key (32 hex chars) ``` ### `tell read` Decode disk binary files written by the [disk sink](/pipeline/sinks/disk). ```bash theme={null} tell read /data/disk/1/2025-01-15/events.bin tell read /data/disk/1/2025-01-15/ ``` ### `tell telemetry` Show what telemetry data Tell would send. For transparency — inspect the exact payload before enabling. ```bash theme={null} tell telemetry show tell telemetry show --json ``` # Installation Source: https://docs.tell.rs/tools/installation Build from source and feature profiles for specialized deployments. The fastest way to install Tell is the one-liner in the [Quickstart](/getting-started/quickstart). This page covers building from source and custom feature profiles. ## Build from source Requires Rust 1.80+ and Cargo: ```bash theme={null} git clone https://github.com/tell-rs/tell.git cd tell cargo install --path crates/tell ``` This installs the `tell` binary with all features enabled — sources, sinks, analytics, TUI, MCP, and LLM. ## Feature profiles Build with a reduced feature set for specific deployments: ```bash theme={null} # Full installation (default) — all sources, sinks, analytics, TUI, MCP, LLM cargo install --path crates/tell # OT collector — TCP + syslog sources, disk + forwarder sinks cargo install --path crates/tell --no-default-features --features ot-collector # OT forwarder — TCP + syslog sources, forwarder sink only cargo install --path crates/tell --no-default-features --features ot-forwarder ``` The `ot-collector` and `ot-forwarder` profiles produce smaller binaries suited for edge deployments and air-gapped networks. ## Verify ```bash theme={null} tell --help tell --version ``` With no arguments, `tell` opens the [TUI](/tools/tui). Use `tell run` to start the server in daemon mode. ## Configuration lookup Tell looks for config files in this order: 1. `--config ` (explicit flag) 2. `configs/config.toml` (current directory) 3. `config.toml` (current directory) If no config file is found, Tell starts with defaults: TCP source on port 50000 routing to stdout. See [Pipeline Configuration](/pipeline/configuration) for the full config reference. # Live Tail Source: https://docs.tell.rs/tools/live-tail Stream data in real time with filters, sampling, and regex match. Stream events, logs, and syslog messages as they flow through Tell's pipeline. The tail connects to a running Tell server via Unix socket and decodes batches in real time — useful for debugging, monitoring deploys, and verifying that data is flowing correctly. ```bash theme={null} tell tail ``` This streams everything. In production, you'll want filters. ## Filtering **Server-side filters** are applied before data leaves the server — use these to reduce volume: ```bash theme={null} # Only events from workspace 1 tell tail --workspace 1 # Only error logs from the HTTP source tell tail --type log --level error --source http # 1% sample at max 100 batches/sec tell tail --sample 0.01 --rate-limit 100 ``` **Client-side filters** match content after decoding: ```bash theme={null} # Events matching a glob pattern tell tail --event "purchase_*" # Regex match across any string field tell tail --match "timeout|connection refused" # Combine: error logs mentioning payments tell tail --type log --level error --match "payment" ``` All filter flags are repeatable — `--workspace 1 --workspace 2` streams from both. ## Output formats ```bash theme={null} tell tail # colored text (default) tell tail --output json # one JSON object per event tell tail --output compact # single-line summary per event tell tail --output raw # raw decoded FlatBuffer data tell tail --metadata-only # batch metadata without payload decode ``` Use `--output json` for piping into `jq` or other tools: ```bash theme={null} tell tail --type event --output json | jq '.event_name' ``` ## Replay Replay the last N batches from the server's ring buffer on connect: ```bash theme={null} tell tail --last 50 ``` This is useful for catching up on what just happened without waiting for new data. ## Setup The tail connects to Tell's tap server via Unix socket. The tap server starts automatically with `tell run` — no extra configuration needed. The default socket path is derived from your data directory. To connect to a specific socket: ```bash theme={null} tell tail --socket /tmp/tell-tap.sock ``` `tell tail` is available on Unix systems only (macOS, Linux). It requires the `tail` feature flag at build time, which is included in the default build. ## What's next * [CLI commands](/tools/commands#tell-tail) — full flag reference for `tell tail` * [Pipeline overview](/pipeline/overview) — understand what flows through the pipeline * [Sources overview](/pipeline/sources/overview) — where the data you're tailing comes from # MCP Server Source: https://docs.tell.rs/tools/mcp 21 tools for AI assistants via stdio and HTTP/SSE transports. Tell includes an MCP (Model Context Protocol) server that gives AI assistants direct access to your analytics. Connect it to Claude Desktop, Cursor, Zed, or any MCP-compatible client — then ask questions about your data in natural language. ## Quick start ### Stdio (Claude Desktop, Cursor, Zed) Add Tell to your MCP client configuration: ```json theme={null} { "mcpServers": { "tell": { "command": "tell", "args": ["mcp"], "env": { "TELL_API_KEY": "tell_your_api_key" } } } } ``` ### HTTP/SSE If your Tell server has MCP enabled, connect via HTTP: ``` POST https://your-tell-server/api/v1/mcp ``` The HTTP transport supports SSE for streaming responses with automatic reconnection. ## What you can do Once connected, ask your AI assistant things like: * "Show me daily active users for the last 30 days" * "Which events are tracked most frequently?" * "Find users most likely to churn" * "Create a dashboard with DAU, WAU, and top events" * "Are there any anomalies in the logs?" The assistant uses Tell's 21 tools behind the scenes to query data, build dashboards, and surface insights. ## Tools ### Workspace discovery | Tool | What it does | | - | - | | `list_workspaces` | List workspaces you have access to with your role in each | ### Data discovery and querying | Tool | What it does | | - | - | | `workspace_info` | Get the complete data model — tables, columns, relationships, query patterns. Call this first before writing SQL. | | `list_columns` | List column names and types for a specific table (Admin+) | | `query` | Execute a read-only SQL query against your analytics data (Admin+) | ### Boards | Tool | What it does | | - | - | | `list_boards` | List dashboards in a workspace | | `get_board` | Get a board's full configuration — blocks, series, visualizations | | `create_board` | Create a new dashboard with metric blocks | | `update_board` | Update a board's title, description, or blocks | | `delete_board` | Permanently delete a board | | `run_board` | Execute all metric series in a board and return the results | ### Sharing | Tool | What it does | | - | - | | `share_board` | Create a public share link for a board | | `get_share_link` | Check if a board has an existing share link | ### Data exploration | Tool | What it does | | - | - | | `list_events` | Discover top events by count and percentage | | `explore_data` | Browse raw rows from events, sessions, users, or logs with filters | ### ML enrichment | Tool | What it does | | - | - | | `enrichment_status` | Check if ML predictions are active and how many events are enriched | | `churn_risk` | Find users most likely to churn, sorted by risk score | | `anomaly_summary` | Summarize anomalous log patterns by source | | `segment_overview` | Show user distribution across ML-assigned segments | ### User segments | Tool | What it does | | - | - | | `list_segments` | List available segment definitions (lifecycle and engagement) | | `segment_counts` | Count users per segment with percentages | | `segment_users` | List users in a specific segment with stats | ## Authentication Tools authenticate using an API key. You can provide it in three ways: 1. **Environment variable** — set `TELL_API_KEY` in your MCP client config 2. **Per-tool parameter** — pass `api_key` to any tool call 3. **Environment reference** — use `env:MY_VAR_NAME` to read from an environment variable Workspace-scoped API keys restrict access to a single workspace. User-level keys allow access to all workspaces the user belongs to. ## Permissions Most tools work with any authenticated user. Two tools require Admin or higher: | Tool | Required role | | - | - | | `query` | Admin+ (raw SQL access) | | `list_columns` | Admin+ (schema inspection) | Board updates and deletions require board ownership or Admin role. ## Recommended workflow The MCP server includes instructions that guide AI assistants through the best workflow: 1. **`list_workspaces`** — find your workspace ID 2. **`workspace_info`** — learn the data model before querying 3. **`run_board`** — use existing dashboards for standard metrics (DAU, MAU, events) 4. **`list_events`** — discover what's being tracked 5. **`query`** — write SQL for ad-hoc analysis 6. **`explore_data`** — drill down into raw data 7. **`enrichment_status`**, **`churn_risk`** / **`anomaly_summary`** — check ML insights ## SQL query security The `query` tool enforces multiple layers of protection: * **SELECT only** — no INSERT, UPDATE, DELETE, DROP, or ALTER * **Workspace scoping** — queries are automatically scoped to your workspace * **Function blocking** — 40+ dangerous functions are blocked (file access, remote connections, system info) * **No cross-workspace access** — queries can't reference other workspace databases All queries are [audit logged](/running-tell/audit-privacy) with execution time and row count. ## What's next * [Blocks](/tools/blocks) — the streaming block format that MCP tools use for generative UI * [CLI Commands](/tools/commands) — `tell mcp` and other CLI tools * [Ask](/tools/ask) — query your data with AI from the command line # Skills Source: https://docs.tell.rs/tools/skills Load and manage custom LLM skills from markdown files. Load and manage custom LLM skills from markdown files with `tell skill`. # TUI Mode Source: https://docs.tell.rs/tools/tui Interactive terminal UI for monitoring Tell's pipeline, querying metrics, and managing boards. Tell includes an interactive terminal UI built with Ratatui. Launch it with no arguments or the `--interactive` flag: ```bash theme={null} tell tell --interactive tell tui --config configs/prod.toml ``` The TUI provides a dashboard view of your pipeline, boards, and metrics without leaving the terminal. It requires the `ui-tui` feature flag at build time, which is included in the default build. For CLI-based analytics without the interactive UI, see [CLI commands](/tools/commands). # Events & Properties Source: https://docs.tell.rs/tracking/events-and-properties Tell's event model — named events with custom key-value properties. Events are the core of Tell's analytics. Every user action you want to measure — a page view, a purchase, a feature toggle — is tracked as a named event with optional properties. ## Tracking an event ```javascript theme={null} tell.track('sign_up', { plan: 'free', source: 'landing_page' }); ``` The first argument is the event name. The second is a properties object — any key-value pairs that add context. Properties can be strings, numbers, booleans, or nested objects. ## Event types SDKs support several event types beyond basic tracking: ### track Record a user action. This is what you'll use most. ```javascript theme={null} tell.track('page_view', { url: '/pricing', referrer: '/home' }); tell.track('purchase', { plan: 'pro', amount: 49.99 }); ``` ### identify Link a device to a known user. Call this when a user logs in or signs up. ```javascript theme={null} tell.identify('user-123', { email: 'alex@example.com', name: 'Alex', plan: 'pro' }); ``` The second argument sets **user traits** — persistent properties associated with the user. Traits are stored separately from event properties and appear in user analytics. ### group Associate a user with a company or team. ```javascript theme={null} tell.group('company-456', { name: 'Acme Corp', plan: 'enterprise', employees: 150 }); ``` ### revenue Track a purchase with structured fields. ```javascript theme={null} tell.revenue({ amount: 49.99, currency: 'USD', order_id: 'order-789' }); ``` ### alias Merge two user identities. Use this when you discover that two device IDs belong to the same person. ```javascript theme={null} tell.alias('anonymous-device-id', 'known-user-id'); ``` ### context Send device and session information. SDKs typically send this automatically. ```javascript theme={null} tell.context({ os: 'iOS', os_version: '17.2', app_version: '2.1.0', device_type: 'mobile' }); ``` ## Properties Properties are key-value pairs attached to events. They power breakdowns, filters, and custom metrics. ### Common patterns ```javascript theme={null} // Page tracking tell.track('page_view', { url: '/pricing', title: 'Pricing' }); // Feature usage tell.track('feature_used', { feature: 'dark_mode', enabled: true }); // E-commerce tell.track('add_to_cart', { product_id: 'sku-123', price: 29.99, quantity: 2 }); ``` ### Super properties Register properties that are automatically attached to every event: ```javascript theme={null} tell.register({ app_version: '2.1.0', environment: 'production' }); ``` This saves you from passing the same properties on every `track` call. ### Before-send hooks Modify or drop events before they leave the SDK: ```javascript theme={null} tell.beforeSend((event) => { // Remove sensitive data delete event.properties.email; return event; // return null to drop the event }); ``` ## Property breakdowns Once your events have properties, you can break down any metric by any property value: * "Daily active users by `country`" * "Event count broken down by `plan`" * "Revenue grouped by `source`" See [Filtering](/analytics/filtering) for the full set of operators (equals, contains, greater than, regex, and more). ## Property aggregation For numeric properties, you can compute aggregations: * **Sum** — total revenue from `purchase.amount` * **Average** — mean order value * **Min / Max** — smallest and largest values * **Count** — number of events with the property set These work through the [Data Queries API](/api/queries) and on [dashboard boards](/analytics/boards). ## What's next * [Users & Identity](/tracking/users-and-identity) — how identity resolution works * [Sessions](/tracking/sessions) — session tracking details * [SDKs](/tracking/sdks/javascript) — language-specific integration guides * [Logs](/tracking/logs) — structured logging (separate protocol) # Cloudflare Source: https://docs.tell.rs/tracking/integrations/cloudflare Monitor DNS, traffic, security, and cache performance from Cloudflare. Track DNS performance, traffic patterns, cache efficiency, and threat activity over time. **Category:** Monitoring · **Auth:** API token · **Metrics:** 25 ## Prerequisites You need two things from your Cloudflare account: 1. **API Token** — Go to [Cloudflare Dashboard → API Tokens](https://dash.cloudflare.com/profile/api-tokens), create a token with **Zone Analytics:Read** permission. 2. **Zone ID** — Found on the **Overview** tab of your domain in the Cloudflare dashboard. ## Configuration Go to **Settings → Integrations → Cloudflare** and enter your API token and zone ID. ```toml theme={null} [[plugins]] name = "cloudflare" entity = "all" config.api_token = "${CLOUDFLARE_API_TOKEN}" config.zone_id = "your-zone-id" ``` Secret values like API tokens can use environment variable references (`${VAR}`). ## Entities Pull everything with `all`, or pick specific categories: | Entity | Description | | - | - | | `dns` | DNS query analytics | | `traffic` | HTTP traffic metrics | | `security` | Firewall and threat metrics | | `performance` | Cache and status code metrics | | `all` | All of the above | ## Metrics ### DNS (8) | Metric | Description | | - | - | | `dns_queries_total` | Total DNS queries (24h) | | `dns_queries_uncached` | Uncached DNS queries (24h) | | `dns_queries_stale` | Stale DNS queries (24h) | | `dns_response_time_avg` | Average DNS response time in ms (24h) | | `dns_response_time_median` | Median DNS response time in ms (24h) | | `dns_response_time_p90` | P90 DNS response time in ms (24h) | | `dns_response_time_p99` | P99 DNS response time in ms (24h) | | `dns_queries_nxdomain` | NXDOMAIN DNS responses (24h) | ### Traffic (7) | Metric | Description | | - | - | | `requests_total` | Total HTTP requests (24h) | | `requests_cached` | Cached HTTP requests (24h) | | `requests_uncached` | Uncached HTTP requests (24h) | | `bandwidth_total` | Total bandwidth in bytes (24h) | | `bandwidth_cached` | Cached bandwidth in bytes (24h) | | `pageviews_total` | Total page views (24h) | | `visitors_unique` | Unique visitors (24h) | ### Security (4) | Metric | Description | | - | - | | `threats_total` | Total threats detected (24h) | | `firewall_events` | Total firewall events (24h) | | `firewall_blocks` | Firewall block actions (24h) | | `firewall_challenges` | Firewall challenge actions (24h) | ### Performance (6) | Metric | Description | | - | - | | `cache_hit_ratio` | Cache hit ratio as percentage (24h) | | `bandwidth_saved_ratio` | Bandwidth saved ratio as percentage (24h) | | `status_2xx` | 2xx status code count (24h) | | `status_3xx` | 3xx status code count (24h) | | `status_4xx` | 4xx status code count (24h) | | `status_5xx` | 5xx status code count (24h) | # GitHub Source: https://docs.tell.rs/tracking/integrations/github Track repository activity — stars, PRs, commits, and issue velocity. Track repo growth, development velocity, and community activity over time. **Category:** Source Control · **Auth:** Personal access token (optional) · **Metrics:** 13 ## Prerequisites * **Personal Access Token** (optional) — Go to [GitHub → Settings → Developer settings → Personal access tokens](https://github.com/settings/tokens). A token increases the rate limit from 60 to 5,000 requests per hour. Not required for public repos. ## Configuration Go to **Settings → Integrations → GitHub** and enter your token (optional) and the repositories you want to track. ```toml theme={null} [[plugins]] name = "github" entity = "your-org/your-repo" config.token = "${GITHUB_TOKEN}" ``` The token is optional for public repositories. You can track multiple repos by adding multiple `[[plugins]]` entries. ## Metrics | Metric | Description | | - | - | | `stars` | Stargazer count | | `forks` | Fork count | | `watchers` | Watcher count | | `open_issues` | Open issues count | | `size` | Repository size in KB | | `default_branch` | Default branch name | | `commits` | Commits in last 30 days | | `last_commit_at` | Timestamp of most recent commit | | `issues_open` | Open issue count | | `issues_closed` | Closed issue count | | `prs_open` | Open PR count | | `prs_closed` | Closed PR count | | `prs_merged` | Merged PR count | # Installation Source: https://docs.tell.rs/tracking/integrations/installation Install and manage Tell integrations. ## Install an Integration Go to **Settings → Integrations**, find the integration you want, and click **Install**. ```bash theme={null} tell plugin install ``` For example: ```bash theme={null} tell plugin install cloudflare ``` ## Available Integrations | Integration | Description | | - | - | | [Cloudflare](/tracking/integrations/cloudflare) | DNS, traffic, security, and cache performance | | [GitHub](/tracking/integrations/github) | Stars, PRs, commits, and issue tracking | | [Resend](/tracking/integrations/resend) | Email delivery, domains, and audiences | | [Shopify](/tracking/integrations/shopify) | Orders, revenue, customers, and inventory | ## Managing Integrations Go to **Settings → Integrations** to view, configure, or remove installed integrations. ```bash theme={null} # List installed integrations tell plugin list # View details for an integration tell plugin info cloudflare # Pull metrics on demand tell plugin pull cloudflare --entity all # Remove an integration tell plugin remove cloudflare ``` # Integrations Source: https://docs.tell.rs/tracking/integrations/overview Pull metrics from Cloudflare, GitHub, Shopify, Resend, and more — no SDK needed. Connect Tell to external services and pull metrics automatically. Set up once, and Tell captures data on a schedule so you can track trends alongside your product analytics. Monitor DNS, traffic, security, and cache performance. Track stars, PRs, commits, and issue velocity. See email delivery, domains, and audience growth. Watch orders, revenue, customers, and inventory. ## Getting Started 1. Go to **Settings → Integrations** 2. Pick a service and click **Connect** 3. Paste your API credentials 4. Choose what to pull and how often Install the integration and add it to your config: ```bash theme={null} tell plugin install cloudflare ``` ```toml theme={null} [[plugins]] name = "cloudflare" entity = "all" config.api_token = "${CLOUDFLARE_API_TOKEN}" config.zone_id = "abc123" ``` Pull metrics manually or let the schedule handle it: ```bash theme={null} tell plugin pull cloudflare --entity all ``` See [Installation](/tracking/integrations/installation) for the full list of available integrations. ## What You Get Once connected, Tell pulls metrics on your configured schedule and tracks them over time. * **Dashboards** — build boards that mix integration data with your product analytics * **Trends** — see how metrics change over days, weeks, and months ## Further Reading * [Installation](/tracking/integrations/installation) — install and manage integrations * [Pipeline Overview](/pipeline/overview) — how integration data flows through the pipeline * [Configuration](/running-tell/configuration) — full config reference # Resend Source: https://docs.tell.rs/tracking/integrations/resend Track email delivery, domains, and audience growth from Resend. Track email volume, domain health, and audience growth over time. **Category:** Email · **Auth:** API key · **Metrics:** 7 ## Prerequisites * **API Key** — Go to [Resend → API Keys](https://resend.com/api-keys) and create a key. It will start with `re_`. ## Configuration Go to **Settings → Integrations → Resend** and enter your API key. ```toml theme={null} [[plugins]] name = "resend" entity = "all" config.api_key = "${RESEND_API_KEY}" ``` ## Entities Pull everything with `all`, or pick specific categories: | Entity | Description | | - | - | | `emails` | Email sending metrics | | `domains` | Domain verification status | | `audiences` | Audience list metrics | | `broadcasts` | Broadcast campaign metrics | | `all` | All of the above | ## Metrics ### Emails | Metric | Description | | - | - | | `emails_total` | Total emails sent | ### Domains | Metric | Description | | - | - | | `domains_total` | Total configured domains | | `domains_verified` | Verified domains count | ### Audiences | Metric | Description | | - | - | | `audiences_total` | Total audience lists | ### Broadcasts | Metric | Description | | - | - | | `broadcasts_total` | Total broadcast campaigns | | `broadcasts_sent` | Sent broadcast count | | `broadcasts_draft` | Draft broadcast count | # Shopify Source: https://docs.tell.rs/tracking/integrations/shopify Track orders, revenue, customers, and inventory from your Shopify store. Track revenue trends, order volume, customer growth, and inventory levels over time. **Category:** Commerce · **Auth:** Admin API access token · **Metrics:** 16 ## Prerequisites 1. **Store domain** — Your `*.myshopify.com` domain (e.g., `mystore.myshopify.com`). 2. **Admin API access token** — Create a [custom app](https://help.shopify.com/en/manual/apps/app-types/custom-apps) in your Shopify admin with read access to orders, products, customers, and inventory. ## Configuration Go to **Settings → Integrations → Shopify** and enter your store domain and access token. ```toml theme={null} [[plugins]] name = "shopify" entity = "all" config.store = "mystore.myshopify.com" config.access_token = "${SHOPIFY_ACCESS_TOKEN}" ``` ## Entities Pull everything with `all`, or pick specific categories: | Entity | Description | | - | - | | `orders` | Order and revenue metrics | | `products` | Product catalog metrics | | `customers` | Customer metrics | | `operations` | Fulfillment, refunds, and inventory | | `all` | All of the above | ## Metrics ### Orders (6) | Metric | Description | | - | - | | `orders_total` | Total order count (all time) | | `orders_today` | Orders placed today | | `orders_30d` | Orders in last 30 days | | `revenue_today` | Revenue today | | `revenue_30d` | Revenue in last 30 days | | `average_order_value` | Average order value (30d) | ### Products (2) | Metric | Description | | - | - | | `products_total` | Total product count | | `products_active` | Active product count | ### Customers (2) | Metric | Description | | - | - | | `customers_total` | Total customer count | | `customers_30d` | New customers in last 30 days | ### Operations (6) | Metric | Description | | - | - | | `unfulfilled_orders` | Unfulfilled order count | | `refunds_30d` | Refund count in last 30 days | | `refund_amount_30d` | Refund amount in last 30 days | | `abandoned_checkouts` | Abandoned checkout count | | `inventory_items` | Total inventory item count | | `low_stock_count` | Items with fewer than 10 in stock | # Logs Source: https://docs.tell.rs/tracking/logs Structured logging with severity levels, service tags, and session correlation. Logs are a separate protocol from events. Events track what users do (product analytics). Logs track what your application does (errors, warnings, system behavior). They use the same SDKs and the same pipeline, but they have a different schema and different semantics. ## Sending a log ```javascript theme={null} tell.log('error', 'Connection to payment gateway timed out', { service: 'billing', error: 'GATEWAY_TIMEOUT', retry_count: 3 }); ``` Every log entry has: | Field | Required | Description | | - | - | - | | **level** | Yes | Severity level (see below) | | **message** | Yes | Human-readable log message | | **service** | Yes | Service or application name | | **data** | No | Structured key-value metadata | ## Severity levels Tell uses RFC 5424 severity levels plus trace: | Level | When to use | | - | - | | `emergency` | System is unusable | | `alert` | Immediate action needed | | `critical` | Critical conditions | | `error` | Error that needs attention | | `warning` | Something unexpected | | `notice` | Normal but significant | | `info` | Informational (default) | | `debug` | Debug details | | `trace` | Detailed tracing | SDKs provide convenience methods for each level: ```javascript theme={null} tell.logError('billing', 'Payment declined', { error: 'CARD_DECLINED' }); tell.logWarning('auth', 'Rate limit approaching', { current: 450, limit: 500 }); tell.logInfo('deploy', 'Service restarted', { version: '2.1.0' }); ``` ## How logs differ from events Logs and events are different protocols with different schemas. Understanding the differences helps you use each one correctly. | | Events | Logs | | - | - | - | | **Purpose** | What users do | What your app does | | **User ID** | Required | Not included | | **Device ID** | Automatic | Not included | | **Session ID** | Automatic | Included (for correlation) | | **Super properties** | Yes (`register()`) | No | | **Identify / Group / Revenue** | Yes | No | | **Device context** | Automatic on mobile | No | | **Service name** | No | Required | | **Severity level** | No | Required | Events are user-centric — every event is tied to a device and a user. Logs are infrastructure-centric — they're tied to a service and a source, not a user. ## Session correlation Logs share a session ID with events. On mobile SDKs, this happens automatically — the same session that groups a user's events also tags their logs. This lets you correlate what a user was doing (events) with what the app was doing (logs) during the same session. On server-side SDKs, you control the session ID. You can set it to match a request ID, a job ID, or any identifier that makes sense for your correlation needs. ```go theme={null} // Server-side: set session for correlation client.ResetSession() // generates new session ID client.Track("checkout_started", userID, props) client.LogError("billing", "Payment timeout", data) // same session ID ``` ## Structured data The `data` field accepts arbitrary key-value pairs — strings, numbers, booleans, nested objects. Use it for context that helps you debug: ```javascript theme={null} tell.logError('api', 'Request failed', { method: 'POST', path: '/api/v1/orders', status: 502, duration_ms: 12340, upstream: 'payment-service' }); ``` ## Pipeline processing Logs flow through the same pipeline as events — routing, transforms, and sinks all apply. You can: * Route logs to a different sink than events (e.g., logs to disk, events to ClickHouse) * Redact PII from log messages using the [redact transform](/pipeline/transforms/redact) * Extract patterns from logs using the [pattern transform](/pipeline/transforms/pattern) for anomaly detection * Filter out noisy log levels using the [filter transform](/pipeline/transforms/filter) See [Pipeline Overview](/pipeline/overview) for configuration details. ## What's next * [Events & Properties](/tracking/events-and-properties) — the product analytics protocol * [Integrations](/tracking/integrations/overview) — pull external data automatically * [SDKs](/tracking/sdks/javascript) — language-specific integration guides # Tracking Overview Source: https://docs.tell.rs/tracking/overview Tell's three data protocols — events, logs, and snapshots — and how to send data. Tell has three data protocols. Each one is a separate schema with its own semantics, but they all flow through the same SDKs and pipeline. | Protocol | What it tracks | How data arrives | | - | - | - | | **Events** | User behavior — page views, signups, purchases, feature usage | SDKs (`track`, `identify`, `group`, `revenue`) | | **Logs** | Application behavior — errors, warnings, system messages | SDKs (`log`, `logError`, `logInfo`, etc.) | | **Snapshots** | External data — GitHub stars, Shopify orders, business metrics | Integrations (scheduled pulls) | ## Events Events are the product analytics protocol. Every event is tied to a user and a device. Events support identity resolution (`identify`), session tracking, super properties, and all the breakdowns and aggregations in Tell's analytics. Read more: [Events & Properties](/tracking/events-and-properties) | [Users & Identity](/tracking/users-and-identity) | [Sessions](/tracking/sessions) ## Logs Logs are the structured logging protocol. Logs are tied to a service and a severity level, not a user. They share a session ID with events for correlation — you can see what your app was doing when a user hit a problem — but they don't carry user identity or device context. Read more: [Logs](/tracking/logs) ## Snapshots Snapshots pull data from external services on a schedule via integrations. No SDK needed — connect a service like GitHub or Shopify and Tell captures metrics over time automatically. Read more: [Integrations](/tracking/integrations/overview) ## SDKs Events and logs are sent through the same SDK. Pick the one for your platform: Node.js, browsers, React, Next.js Backend services iOS, macOS Cross-platform mobile Rust services Native applications # C++ SDK Source: https://docs.tell.rs/tracking/sdks/cpp Track events and logs from C++ applications with sub-microsecond overhead and zero dependencies. The C++ SDK sends events and structured logs from your services to Tell. It's a server SDK — you create a client, pass a user ID on every call, and a background worker thread handles batching and delivery over TCP. Your thread does about 85 ns of work per call and never touches the network. The SDK has zero external dependencies beyond C++17 and pthreads. ## Installation Add the SDK as a CMake subdirectory: ```cmake theme={null} add_subdirectory(tell-cpp) target_link_libraries(your_target PRIVATE tell) ``` Or build from source: ```bash theme={null} cmake -B build -DCMAKE_BUILD_TYPE=Release cmake --build build ``` Requires a C++17 compiler (GCC 7+, Clang 5+, MSVC 2017+) and CMake 3.14+. ## Quick start ```cpp theme={null} #include "tell/tell.hpp" int main() { auto client = tell::Tell::create( tell::TellConfig::production("a1b2c3d4e5f60718293a4b5c6d7e8f90") ); client->track("user_123", "Page Viewed", tell::Props().add("url", "/home").add("referrer", "google")); client->close(); } ``` The client connects to `collect.tell.rs:50000`, batches up to 100 events, and flushes every 10 seconds or when you call `close()`. ## Configuration Use one of the two presets, or build a custom config. ```cpp theme={null} // Production — collect.tell.rs:50000, batch 100, flush 10s auto config = tell::TellConfig::production("a1b2c3d4e5f60718293a4b5c6d7e8f90"); // Development — localhost:50000, batch 10, flush 2s auto config = tell::TellConfig::development("a1b2c3d4e5f60718293a4b5c6d7e8f90"); ``` For custom settings, use the builder: ```cpp theme={null} auto config = tell::TellConfig::builder("a1b2c3d4e5f60718293a4b5c6d7e8f90") .endpoint("collect.internal:50000") .batch_size(200) .flush_interval(std::chrono::milliseconds(5000)) .max_retries(5) .close_timeout(std::chrono::milliseconds(10000)) .network_timeout(std::chrono::milliseconds(15000)) .on_error([](const tell::TellError& e) { std::cerr << "[Tell] " << e.what() << std::endl; }) .build(); ``` The API key must be a 32-character hex string. The SDK validates it at build time and throws `TellError` if it's invalid. ## Tracking events Every tracking method takes `user_id` as its first parameter. Calls never block or throw — errors go to the optional `on_error` callback. ```cpp theme={null} // Track a custom event client->track("user_123", "Feature Used", tell::Props().add("feature", "dark_mode").add("enabled", true)); // Identify a user client->identify("user_123", tell::Props().add("name", "Jane").add("plan", "pro")); // Associate user with a group client->group("user_123", "company_456", tell::Props().add("plan", "enterprise").add("seats", 50)); // Track revenue client->revenue("user_123", 49.99, "USD", "order_789", tell::Props().add("product", "annual_plan")); // Link two user identities client->alias("anon_abc", "user_123"); ``` Properties are always optional — you can omit them entirely: ```cpp theme={null} client->track("user_123", "Click"); ``` ### Standard event names The SDK provides typed constants for common events: ```cpp theme={null} client->track("user_123", tell::Events::PAGE_VIEWED, tell::Props().add("url", "/pricing")); client->track("user_123", tell::Events::USER_SIGNED_UP, tell::Props().add("source", "organic")); client->track("user_123", tell::Events::ORDER_COMPLETED, tell::Props().add("total", 99.00)); ``` Constants cover user lifecycle, revenue, subscriptions, trials, shopping, engagement, and communication events. Custom string names are always accepted too. ### Super properties Register properties that get merged into every `track`, `group`, and `revenue` call: ```cpp theme={null} client->register_props( tell::Props().add("app_version", "2.1.0").add("env", "production")); // This track call automatically includes app_version and env client->track("user_123", "Click", tell::Props().add("button", "submit")); // Remove a super property client->unregister("env"); ``` Event-specific properties override super properties when keys conflict. ## Structured logging Send logs alongside events through the same pipeline. Each log has an RFC 5424 severity level. ```cpp theme={null} client->log_error("DB connection failed", "api", tell::Props().add("host", "db.internal").add("retries", 3)); client->log_info("Request processed", "api", tell::Props().add("status", 200).add("duration_ms", 45)); client->log_warning("Rate limit approaching", "gateway"); ``` The `service` parameter defaults to `"app"` if omitted. Convenience methods are available for all nine levels: `log_emergency`, `log_alert`, `log_critical`, `log_error`, `log_warning`, `log_notice`, `log_info`, `log_debug`, and `log_trace`. You can also use the generic `log` method with an explicit level: ```cpp theme={null} client->log(tell::LogLevel::Notice, "Deployment completed", "deploy", tell::Props().add("version", "3.1.0").add("commit", "abc123f")); ``` ## Properties The `Props` class builds key-value pairs with a chainable API. It writes JSON bytes directly to a buffer — no intermediate allocations. ```cpp theme={null} tell::Props() .add("url", "/home") // string .add("count", 42) // int .add("active", true) // bool .add("score", 3.14); // double ``` Supported types: `std::string`, `const char*`, `int`, `int64_t`, `double`, and `bool`. ## Lifecycle ```cpp theme={null} // Force-send all queued events and logs client->flush(); // Rotate session ID client->reset_session(); // Flush + shut down the background worker client->close(); ``` Always call `close()` before your process exits to avoid losing buffered events. It blocks up to `close_timeout` (default 5 seconds). ## Thread safety The SDK is fully thread-safe. Internal state is protected by `std::shared_mutex`: ```cpp theme={null} auto client = tell::Tell::create(config); std::thread t1([&] { client->track("user_1", "Event A"); }); std::thread t2([&] { client->track("user_2", "Event B"); }); std::thread t3([&] { client->log_info("Processing", "worker"); }); t1.join(); t2.join(); t3.join(); client->close(); ``` ## Error handling Construction throws on invalid config: ```cpp theme={null} try { auto client = tell::Tell::create(config); } catch (const tell::TellError& e) { std::cerr << e.what() << std::endl; } ``` Tracking and logging calls (`track`, `identify`, `log_*`, etc.) never throw. Invalid input (empty user ID, event name too long) is reported through the `on_error` callback: ```cpp theme={null} auto config = tell::TellConfig::builder("a1b2c3d4e5f60718293a4b5c6d7e8f90") .on_error([](const tell::TellError& e) { if (e.kind() == tell::ErrorKind::Validation) { std::cerr << "Invalid " << e.field() << ": " << e.reason() << std::endl; } }) .build(); ``` ## Advanced ### Configuration reference | Parameter | Default (production) | Default (development) | Description | | - | - | - | - | | `endpoint` | `collect.tell.rs:50000` | `localhost:50000` | TCP collector address | | `batch_size` | 100 | 10 | Events per batch before auto-flush | | `flush_interval` | 10s | 2s | Time between auto-flushes | | `max_retries` | 3 | 3 | Retry attempts on send failure | | `close_timeout` | 5s | 5s | Max wait on `close()` | | `network_timeout` | 30s | 30s | TCP connect/send timeout | | `on_error` | silent | silent | Error callback | ### Validation rules | Field | Rule | | - | - | | API key | 32-character hex string | | User ID | Non-empty | | Event name | 1–256 characters | | Log message | 1–65,536 characters | | Group ID | Non-empty | | Revenue amount | Positive number | | Service | Max 256 characters | ### Performance Caller latency — serialize, encode, and enqueue (Apple M4 Pro): | Operation | Latency | | - | - | | `track` (no props) | 85 ns | | `track` (with props) | 224 ns | | `identify` | 272 ns | | `log` | 273 ns | | `revenue` | 326 ns | ### Retry behavior On TCP send failure, the SDK retries with exponential backoff up to `max_retries` attempts. After exhausting retries, the batch is dropped and the error reported via `on_error`. # Flutter SDK Source: https://docs.tell.rs/tracking/sdks/flutter Track events and logs from Flutter apps on iOS, Android, and web. Track events and logs from Flutter apps on iOS, Android, and web. # Go SDK Source: https://docs.tell.rs/tracking/sdks/go Track events and logs from Go services. The Go SDK sends events and structured logs from your backend services to Tell. It's a server SDK — you create a client, pass a user ID on every call, and a background goroutine handles batching and delivery over TCP. Every method takes a `context.Context` and returns an `error`. The calling goroutine does about 320 ns of work (serialize, encode, enqueue) and never touches the network. ## Installation ```bash theme={null} go get github.com/tell-rs/tell-go ``` Requires Go 1.24+. ## Quick start ```go theme={null} package main import ( "context" "github.com/tell-rs/tell-go" ) func main() { client, err := tell.Production("a1b2c3d4e5f60718293a4b5c6d7e8f90") if err != nil { panic(err) } defer client.Close(context.Background()) client.Track(context.Background(), "user_123", "Page Viewed", tell.Properties{ "url": "/home", "referrer": "google", }) } ``` The client connects to `collect.tell.rs:50000`, batches up to 100 events, and flushes every 10 seconds or when you call `Close`. ## Configuration Use one of the two presets, or pass a custom `Config` struct. ```go theme={null} // Production — collect.tell.rs:50000, batch 100, flush 10s client, err := tell.Production("a1b2c3d4e5f60718293a4b5c6d7e8f90") // Development — localhost:50000, batch 10, flush 2s client, err := tell.Development("a1b2c3d4e5f60718293a4b5c6d7e8f90") ``` Both presets accept an optional error callback: ```go theme={null} client, err := tell.Development("a1b2c3d4e5f60718293a4b5c6d7e8f90", func(e error) { fmt.Println("[Tell]", e) }, ) ``` For custom settings: ```go theme={null} client, err := tell.NewClient("a1b2c3d4e5f60718293a4b5c6d7e8f90", tell.Config{ Endpoint: "collect.internal:50000", BatchSize: 200, FlushInterval: 5 * time.Second, MaxRetries: 5, CloseTimeout: 10 * time.Second, NetworkTimeout: 60 * time.Second, OnError: func(e error) { fmt.Println("[Tell]", e) }, }) ``` The API key must be a 32-character hex string. `NewClient` returns an error if it's invalid. ## Tracking events Every tracking method takes `context.Context` and `userID` as its first parameters. Properties are `tell.Properties` (`map[string]interface{}`) or `nil`. ```go theme={null} ctx := context.Background() // Track a custom event client.Track(ctx, "user_123", "Feature Used", tell.Properties{ "feature": "dark_mode", "enabled": true, }) // Identify a user client.Identify(ctx, "user_123", tell.Properties{ "name": "Jane", "email": "jane@example.com", "plan": "pro", }) // Associate user with a group client.Group(ctx, "user_123", "org_456", tell.Properties{ "name": "Acme Corp", "plan": "enterprise", }) // Track revenue client.Revenue(ctx, "user_123", 49.99, "USD", "order_789", tell.Properties{ "product": "pro_annual", }) // Link two user identities client.Alias(ctx, "anon_visitor_abc", "user_123") ``` Pass `nil` instead of `tell.Properties{}` when you don't need properties: ```go theme={null} client.Track(ctx, "user_123", "App Opened", nil) client.Revenue(ctx, "user_123", 9.99, "USD", "order_790", nil) ``` ### Standard event names The SDK provides constants for common events: ```go theme={null} client.Track(ctx, "user_123", tell.PageViewed, tell.Properties{"url": "/pricing"}) client.Track(ctx, "user_123", tell.UserSignedUp, tell.Properties{"source": "organic"}) client.Track(ctx, "user_123", tell.OrderCompleted, tell.Properties{"total": 99.00}) client.Track(ctx, "user_123", tell.FeatureUsed, tell.Properties{"feature": "export"}) ``` Constants cover user lifecycle, revenue, subscriptions, trials, shopping, engagement, and communication events. Custom string names are always accepted too. ### Super properties Register properties that get merged into every `Track`, `Group`, and `Revenue` call: ```go theme={null} client.Register(tell.Properties{"app_version": "2.1.0", "env": "production"}) // This Track call automatically includes app_version and env client.Track(ctx, "user_123", "Click", tell.Properties{"button": "submit"}) // Remove a super property client.Unregister("env") ``` Event-specific properties override super properties when keys conflict. ## Structured logging Send logs alongside events through the same pipeline. Each log has an RFC 5424 severity level. ```go theme={null} client.LogError(ctx, "DB connection failed", tell.Ptr("api"), map[string]interface{}{ "host": "db.internal", "retries": 3, }) client.LogInfo(ctx, "Request processed", tell.Ptr("api"), map[string]interface{}{ "status": 200, "duration_ms": 45, }) client.LogWarning(ctx, "Rate limit approaching", tell.Ptr("gateway"), nil) ``` The `service` parameter is `*string` — use `tell.Ptr("myservice")` to pass a value, or `nil` to default to `"app"`. Convenience methods are available for all nine levels: `LogEmergency`, `LogAlert`, `LogCritical`, `LogError`, `LogWarning`, `LogNotice`, `LogInfo`, `LogDebug`, and `LogTrace`. For full control, use the `Log` method with a `LogEntry` struct: ```go theme={null} client.Log(ctx, tell.LogEntry{ Level: tell.LogNoticeLevel, Message: "Deployment completed", Service: "deploy", Data: map[string]interface{}{ "version": "3.1.0", "commit": "abc123f", }, }) ``` ## Properties `tell.Properties` is `map[string]interface{}`. Supported value types: * `string` * `int`, `int32`, `int64`, `float32`, `float64` * `bool` * `time.Time` * `[]interface{}` (arrays) * `map[string]interface{}` (nested objects) * `nil` ## Lifecycle ```go theme={null} ctx := context.Background() // Force-send all queued events and logs client.Flush(ctx) // Rotate session ID client.ResetSession() // Flush + shut down the background goroutine client.Close(ctx) ``` Always call `Close` before your process exits to avoid losing buffered events. Use `defer client.Close(ctx)` right after creating the client. ## Goroutine safety The client is safe to share across goroutines: ```go theme={null} client, _ := tell.Production("a1b2c3d4e5f60718293a4b5c6d7e8f90") go func() { client.Track(ctx, "user_1", "Event A", nil) }() go func() { client.Track(ctx, "user_2", "Event B", nil) }() go func() { client.LogInfo(ctx, "Processing", tell.Ptr("worker"), nil) }() ``` ## Error handling Constructor errors are returned directly: ```go theme={null} client, err := tell.Production("bad-key") if err != nil { // Invalid API key, bad endpoint, etc. log.Fatal(err) } ``` Tracking and logging methods return validation errors for invalid input. Network errors are handled internally with retries and reported via the `OnError` callback: ```go theme={null} client, _ := tell.NewClient("a1b2c3d4e5f60718293a4b5c6d7e8f90", tell.Config{ OnError: func(e error) { var ve *types.ValidationError if errors.As(e, &ve) { log.Printf("Validation: %s - %s", ve.Field, ve.Message) } else { log.Printf("Tell error: %v", e) } }, }) ``` ## Advanced ### Configuration reference | Parameter | Default (production) | Default (development) | Description | | - | - | - | - | | `Endpoint` | `collect.tell.rs:50000` | `localhost:50000` | TCP collector address | | `BatchSize` | 100 | 10 | Events per batch before auto-flush | | `FlushInterval` | 10s | 2s | Time between auto-flushes | | `MaxRetries` | 3 | 3 | Retry attempts on send failure | | `CloseTimeout` | 5s | 5s | Max wait on `Close` | | `NetworkTimeout` | 30s | 30s | TCP connect timeout | | `OnError` | silent | silent | Error callback | ### Validation rules | Field | Rule | | - | - | | API key | 32-character hex string | | UserID | Non-empty | | Event name | Non-empty | | GroupID | Non-empty | | Revenue amount | Positive number | | Currency | Non-empty | | OrderID | Non-empty | | Log message or data | At least one required | ### Performance Caller latency — serialize, encode, and enqueue (Apple M4 Pro): | Operation | ns/op | allocs/op | | - | - | - | | `Track` (no props) | 320 | 2 | | `Track` (2 props) | 494 | 2 | | `Track` (14 props) | 1,061 | 2 | | `Identify` | 511 | 2 | | `Revenue` | 554 | 3 | | `Log` (with data) | 421 | 3 | Delivery throughput — batched and sent over TCP: | Batch size | events/sec | | - | - | | 10 | 1.2M/s | | 100 | 1.9M/s | | 500 | 2.2M/s | # Overview Source: https://docs.tell.rs/tracking/sdks/javascript Track events and logs from Node.js backends, browsers, and frontend frameworks. The TypeScript SDK is a family of packages for sending events and structured logs to Tell. Every package is written in TypeScript with full type definitions — plain JavaScript works too. ## Which package do I need? | I'm building... | Install | Guide | | - | - | - | | A Node.js backend or API | `npm i @tell-rs/node` | [Node.js](/tracking/sdks/javascript/node) | | A browser app (vanilla TS/JS) | `npm i @tell-rs/browser` | [Browser](/tracking/sdks/javascript/browser) | | A React SPA | `npm i @tell-rs/react @tell-rs/browser` | [React](/tracking/sdks/javascript/react) | | A Next.js app | `npm i @tell-rs/nextjs @tell-rs/browser` | [Next.js](/tracking/sdks/javascript/nextjs) | | A TanStack Router/Start app | `npm i @tell-rs/react @tell-rs/browser` | [TanStack](/tracking/sdks/javascript/tanstack) | | A Vue app | `npm i @tell-rs/vue @tell-rs/browser` | [Vue](/tracking/sdks/javascript/vue) | Client SDK with sessions, device context, and privacy controls. Server SDK with batching, gzip, and zero dependencies. Provider component and hooks for React apps. App Router component with auto page view tracking. Page view tracking for TanStack Router and Start. Plugin and composable for Vue apps. ## Architecture There are two standalone SDKs for two different runtimes: **Browser SDK** (`@tell-rs/browser`) runs in the browser. It has access to `window`, `document`, `localStorage`, `navigator`, and `sendBeacon`. It automatically manages device IDs, sessions, page visibility, UTM capture, and client-side context (screen, OS, locale, timezone). Data is collected client-side — rich but user-modifiable. **Node.js SDK** (`@tell-rs/node`) runs on the server. It uses HTTP + JSONL for transport, supports gzip, and has zero browser dependencies. Data is collected server-side — trustworthy and tamper-proof. **Framework packages** (`@tell-rs/react`, `@tell-rs/nextjs`, `@tell-rs/vue`) are thin wrappers around the browser SDK. They connect Tell to framework lifecycle (mounting, unmounting, route changes) and provide idiomatic APIs (hooks, composables, providers). This is why they require `@tell-rs/browser` as a peer — the framework package handles integration, the browser SDK handles tracking. ## Further reading Before diving into an SDK guide, you may want to read the global tracking docs first: * [Events & Properties](/tracking/events-and-properties) — event types, when to use each, property patterns * [Users & Identity](/tracking/users-and-identity) — device IDs, user IDs, the identity model * [Sessions](/tracking/sessions) — session lifecycle, timeouts, automatically collected context * [Logs](/tracking/logs) — severity levels, structured data, session correlation * [Audit & Privacy](/running-tell/audit-privacy) — privacy model, PII redaction, GDPR/CCPA ## Validation rules These apply to all packages: | Field | Rule | | - | - | | API key | 32-character hex string | | User ID | Required string (cannot be `null`, `undefined`, or `""`) | | Event name | Required string, 1–256 characters | | Log message | Required string, 1–65,536 characters | | Group ID | Required string | | Revenue amount | Positive number | | Currency | Required string | | Order ID | Required string | # Browser SDK Source: https://docs.tell.rs/tracking/sdks/javascript/browser Client SDK for browsers — analytics events, structured logging, automatic sessions, and privacy controls. ```bash theme={null} npm install @tell-rs/browser ``` A client-side singleton that handles device IDs, sessions, and delivery automatically. No user ID needed on `track` calls. **No cookies. No IP addresses. No cross-site tracking.** Visitors are identified by a random device UUID stored in localStorage. The SDK collects anonymous device metadata only — you choose what properties to send. See [Audit & Privacy](/running-tell/audit-privacy) for the full privacy model. ## Quick start ```typescript theme={null} import { tell } from "@tell-rs/browser"; tell.configure("a1b2c3d4e5f60718293a4b5c6d7e8f90"); tell.track("Page Viewed", { url: "/home" }); tell.identify("user_123", { name: "Jane", plan: "pro" }); ``` Events called before `configure()` are queued and replayed once ready. ### Verify it works Open your browser's DevTools Network tab and look for a `POST` request to `https://collect.tell.app/v1/events`. If you see a `202` response, the SDK is sending data successfully. In development mode, the SDK also logs to the console at `debug` level. ## Script tag (no bundler) If you don't use a bundler, load the SDK directly from a CDN. The global build exposes a `Tell` object on `window`. ```html theme={null} ``` All configuration options, tracking methods, and privacy controls work the same as the npm install — only the loading mechanism differs. ### SPA page view tracking For single-page apps without a framework router, hook into the History API to track navigations: ```html theme={null} ``` ### Pin a version Replace `@latest` with a specific version to avoid unexpected changes in production: ``` https://cdn.jsdelivr.net/npm/@tell-rs/browser@0.3.2/dist/tell.global.js ``` ## Configuration ```typescript theme={null} tell.configure("YOUR_API_KEY", { endpoint: "https://collect.tell.app", batchSize: 20, flushInterval: 5_000, sessionTimeout: 30 * 60 * 1000, maxSessionLength: 24 * 60 * 60 * 1000, persistence: "localStorage", respectDoNotTrack: false, botDetection: true, captureErrors: false, onError: (e) => console.error("[Tell]", e), }); ``` For local development, point the SDK at your Tell server: ```typescript theme={null} tell.configure("YOUR_API_KEY", { endpoint: "http://localhost:8080", batchSize: 5, flushInterval: 2_000, logLevel: "debug", }); ``` ## Tracking events No user ID parameter — the SDK tracks the current user automatically (anonymous until you call `identify`). For a full explanation of the identity model (device ID, anonymous ID, user ID), see [Users & Identity](/tracking/users-and-identity). ```typescript theme={null} tell.track("Feature Used", { feature: "dark_mode", enabled: true }); tell.identify("user_123", { name: "Jane", email: "jane@example.com" }); tell.group("company_456", { plan: "enterprise" }); tell.revenue(49.99, "USD", "order_789", { product: "annual_plan" }); tell.alias("anon_visitor_abc", "user_123"); ``` For details on each event type and when to use them, see [Events & Properties](/tracking/events-and-properties). ### Standard event names ```typescript theme={null} import { tell, Events } from "@tell-rs/browser"; tell.track(Events.PageViewed, { url: "/pricing" }); tell.track(Events.UserSignedUp, { source: "organic" }); tell.track(Events.FeatureUsed, { feature: "export" }); ``` Constants cover user lifecycle, revenue, subscriptions, trials, shopping, engagement, and communication events. Custom string names always work too. ### Super properties Properties automatically attached to every `track`, `group`, and `revenue` call: ```typescript theme={null} tell.register({ app_version: "2.1.0" }); // Every track call now includes app_version tell.track("Click", { button: "submit" }); tell.unregister("app_version"); ``` Event-specific properties override super properties when keys conflict. See [Events & Properties](/tracking/events-and-properties) for more on property patterns. ## Structured logging ```typescript theme={null} tell.logError("Payment failed", { error: "card_declined", amount: 9.99 }); tell.logInfo("User signed in", { method: "oauth" }); tell.logWarning("High memory usage"); ``` Nine severity levels from `logEmergency` to `logTrace`. See [Logs](/tracking/logs) for the full level reference and when to use each. ## Sessions Sessions are managed automatically. The session ID is persisted to localStorage, so page navigations within the same site continue the same session. A new session starts after 30 minutes of inactivity (configurable via `sessionTimeout`) or after 24 hours (configurable via `maxSessionLength`). The SDK also captures UTM parameters from the URL as super properties. For the full session lifecycle model, see [Sessions](/tracking/sessions). ## Data collected automatically The browser SDK captures device context on every session. No PII is collected — all fields are anonymous device and browser metadata. | Data | Example value | Field | | - | - | - | | Full page URL (incl. query strings) | `https://example.com/pricing?ref=blog` | `context.url` | | Page title | `Pricing - Acme` | `context.title` | | Referrer URL | `https://google.com/search?q=acme` | `context.referrer` | | Referrer domain | `google.com` | `context.referrer_domain` | | UTM parameters | `utm_source=twitter` | Super properties | | Browser + version | `Chrome 120` | `context.browser`, `context.browser_version` | | OS + version | `macOS 14.2` | `context.os_name`, `context.os_version` | | Device type | `desktop` | `context.device_type` | | Screen dimensions | `2560 x 1440` | `context.screen_width`, `context.screen_height` | | Viewport dimensions | `1280 x 720` | `context.viewport_width`, `context.viewport_height` | | Device pixel ratio | `2` | `context.device_pixel_ratio` | | CPU cores | `8` | `context.cpu_cores` | | Device memory | `16` (GB) | `context.device_memory` | | Locale | `en-US` | `context.locale` | | Timezone | `America/New_York` | `context.timezone` | | Connection type | `4g` | `context.connection_type` | | Touch support | `false` | `context.touch` | **URLs include query strings.** If your app puts tokens or sensitive data in URLs (e.g. `?token=abc`), use `beforeSend` or the `redact()` utility below to strip them before events leave the browser. ## Redaction & beforeSend The `beforeSend` hook lets you transform or drop events before they're queued. Return the modified event, or `null` to drop it entirely. ### Drop events from routes ```typescript theme={null} tell.configure("YOUR_API_KEY", { beforeSend: (event) => { const url = event.context?.url; if (typeof url === "string" && new URL(url).pathname.startsWith("/internal")) { return null; // drop internal pages } return event; }, }); ``` ### Strip query params from URLs ```typescript theme={null} tell.configure("YOUR_API_KEY", { beforeSend: (event) => { if (event.context?.url && typeof event.context.url === "string") { const u = new URL(event.context.url); u.searchParams.delete("token"); u.searchParams.delete("api_key"); return { ...event, context: { ...event.context, url: u.toString() } }; } return event; }, }); ``` ### Remove PII from properties ```typescript theme={null} tell.configure("YOUR_API_KEY", { beforeSend: (event) => { if (event.traits?.email) { return { ...event, traits: { ...event.traits, email: "[REDACTED]" } }; } return event; }, }); ``` ### Scrub log data ```typescript theme={null} tell.configure("YOUR_API_KEY", { beforeSendLog: (log) => { if (log.data?.password) { return { ...log, data: { ...log.data, password: "[REDACTED]" } }; } return log; }, }); ``` ### User opt-out via beforeSend ```typescript theme={null} tell.configure("YOUR_API_KEY", { beforeSend: (event) => { if (localStorage.getItem("analytics-optout")) return null; return event; }, }); ``` ### `redact()` utility For common redaction patterns, use the built-in `redact()` factory instead of writing `beforeSend` by hand: ```typescript theme={null} import { tell, redact, redactLog, SENSITIVE_PARAMS } from "@tell-rs/browser"; tell.configure("YOUR_API_KEY", { beforeSend: redact({ // Drop events from internal routes dropRoutes: ["/internal", "/health"], // Strip sensitive query params from all URLs stripParams: [...SENSITIVE_PARAMS, "session_id"], // Replace matching property/trait keys with "[REDACTED]" redactKeys: ["email", "phone", "ssn"], }), beforeSendLog: redactLog({ redactKeys: ["password", "credit_card"], }), }); ``` `redact()` and `redactLog()` return standard `beforeSend` functions. You can combine them with your own in an array: ```typescript theme={null} tell.configure("YOUR_API_KEY", { beforeSend: [ redact({ stripParams: SENSITIVE_PARAMS }), myCustomHook, ], }); ``` Client-side redaction is your first line of defense. For defense in depth, also configure [server-side pipeline redaction](/pipeline/transforms/redact) to catch anything that slips through. ## Error auto-capture Set `captureErrors: true` to automatically log uncaught errors and unhandled promise rejections. ```typescript theme={null} tell.configure("YOUR_API_KEY", { captureErrors: true }); ``` ### What it captures **`window.onerror`** — uncaught exceptions: * `message` — the error message * `filename` — source file URL * `lineno` / `colno` — line and column number * `stack` — stack trace (when available) **`unhandledrejection`** — unhandled promise rejections: * `message` — the rejection reason (or `"Unhandled promise rejection"`) * `stack` — stack trace (when the reason is an Error) Both are logged via `logError` with service `"browser"` and appear in your Tell logs dashboard. ### When to use vs. your own error boundary `captureErrors` is a catch-all safety net for errors that escape your application code. If you use React error boundaries or framework error handlers, you may prefer to log to Tell explicitly from those: ```typescript theme={null} tell.logError(error.message, { stack: error.stack, component: "Checkout" }); ``` ## Privacy ```typescript theme={null} tell.optOut(); // Stop all tracking — events are dropped locally tell.optIn(); // Resume tracking tell.isOptedOut(); // Check current state tell.reset(); // Clear user, device ID, session, super properties ``` Opt-out state is persisted to localStorage. Set `respectDoNotTrack: true` to honor the browser's Do Not Track setting. ## Lifecycle ```typescript theme={null} tell.enable(); // Re-enable after disable() tell.disable(); // Pause tracking tell.reset(); // Clear user, device, session (e.g. on logout) await tell.flush(); // Flush pending events await tell.close(); // Flush + shut down ``` ## Error handling Tracking calls (`track`, `identify`, `group`, `revenue`, `alias`) never throw. Validation errors and network failures route through the `onError` callback: ```typescript theme={null} tell.configure("YOUR_API_KEY", { onError: (err) => console.error("[Tell]", err.message), }); ``` ## Retry behavior On HTTP send failure (5xx or network error), the SDK retries with exponential backoff: 1-second base delay, 1.5x multiplier, 20% jitter, capped at 30 seconds. After `maxRetries` attempts (default: 5), the batch is dropped and reported via `onError`. The SDK also checks `navigator.onLine` before each attempt and skips if the browser reports offline. ## Browser-specific features **Bot detection** — automatically disables tracking when `navigator.webdriver` is set or the user agent contains "headless". Controlled by `botDetection` (default: true). **Page unload** — the SDK flushes via `navigator.sendBeacon` on `beforeunload` and `visibilitychange` to minimize data loss. **Pre-init queue** — events called before `configure()` are buffered (up to 1,000) and replayed once the SDK is ready. ## Configuration reference | Parameter | Default | Description | | - | - | - | | `endpoint` | `https://collect.tell.app` | HTTP collector URL | | `batchSize` | 20 | Events per batch before auto-flush | | `flushInterval` | 5,000 ms | Time between auto-flushes | | `maxRetries` | 5 | Retry attempts on send failure | | `closeTimeout` | 5,000 ms | Max wait on `close()` | | `networkTimeout` | 10,000 ms | HTTP request timeout | | `sessionTimeout` | 1,800,000 ms | Inactivity time before new session | | `maxSessionLength` | 86,400,000 ms | Max session duration (prevents zombie sessions) | | `persistence` | `localStorage` | `"localStorage"` or `"memory"` | | `respectDoNotTrack` | false | Honor browser DNT setting | | `botDetection` | true | Auto-disable for bots/headless | | `captureErrors` | false | Auto-log uncaught errors | | `logLevel` | `error` | SDK log verbosity | | `maxQueueSize` | 1,000 | Max queued items before oldest drops | # Next.js SDK Source: https://docs.tell.rs/tracking/sdks/javascript/nextjs Drop-in Next.js App Router component with automatic page view tracking. ```bash theme={null} npm install @tell-rs/nextjs @tell-rs/browser ``` A drop-in client component for the App Router with automatic page view tracking. `@tell-rs/nextjs` is a thin wrapper around the [browser SDK](/tracking/sdks/javascript/browser). It adds a `"use client"` component that watches Next.js route changes and fires `Page Viewed` events automatically. You install `@tell-rs/browser` alongside because that's where the actual tracking, sessions, and transport live. **Client components only.** The Tell browser SDK runs in the browser and requires `window`, `document`, and `localStorage`. It cannot run in React Server Components. For server-side analytics (e.g. tracking API routes or server actions), use the [Node.js SDK](/tracking/sdks/javascript/node) instead. ## Setup ```tsx theme={null} // app/layout.tsx import { Tell } from "@tell-rs/nextjs"; export default function RootLayout({ children }) { return ( {children} ); } ``` The `Tell` component watches `usePathname()` and `useSearchParams()` and fires a `Page Viewed` event on every route change. Disable with `trackPageViews={false}`. ### With options ```tsx theme={null} import { Tell } from "@tell-rs/nextjs"; import { redact, SENSITIVE_PARAMS } from "@tell-rs/browser"; ``` ## Manual tracking ```tsx theme={null} "use client"; import { tell } from "@tell-rs/nextjs"; tell.track("Checkout Started", { items: 3 }); tell.identify("user_123", { plan: "pro" }); ``` ## Server-side tracking For API routes, middleware, or server actions, use `@tell-rs/node` separately: ```typescript theme={null} // app/api/checkout/route.ts import { Tell, production } from "@tell-rs/node"; const tell = new Tell(production("YOUR_API_KEY")); export async function POST(req: Request) { const body = await req.json(); tell.track(body.userId, "Checkout Completed", { total: body.total }); return Response.json({ ok: true }); } ``` ## What you get from the browser SDK All features documented in the [browser SDK guide](/tracking/sdks/javascript/browser) are available — sessions, device context, super properties, structured logging, opt-out, and `beforeSend` hooks. The Next.js package only adds the auto page view component. # Node.js SDK Source: https://docs.tell.rs/tracking/sdks/javascript/node Server SDK for Node.js — analytics events and structured logging with batching and delivery. ```bash theme={null} npm install @tell-rs/node ``` A server-side SDK — you create a client, pass a user ID on every call, and a background worker handles batching and delivery over HTTP + JSONL. Zero dependencies. **Data collected server-side is trustworthy and tamper-proof.** Unlike the browser SDK, the Node.js SDK doesn't auto-collect device context — you control exactly what properties are sent. ## Quick start ```typescript theme={null} import { Tell, production, Events } from "@tell-rs/node"; const tell = new Tell(production("a1b2c3d4e5f60718293a4b5c6d7e8f90")); tell.track("user_123", "Page Viewed", { url: "/home" }); tell.identify("user_123", { name: "Jane", plan: "pro" }); await tell.close(); ``` ### Verify it works In development mode (`logLevel: "debug"`), the SDK logs to the console when events are batched and flushed. You can also call `await tell.flush()` and check that no error is reported via `onError`. ## Configuration Two presets are available: ```typescript theme={null} // Production — default endpoint, batch 100, flush 10s, error-only logging const tell = new Tell(production("YOUR_API_KEY")); // Development — localhost:8080, batch 10, flush 2s, debug logging const tell = new Tell(development("YOUR_API_KEY")); ``` For custom settings: ```typescript theme={null} const tell = new Tell({ apiKey: "YOUR_API_KEY", endpoint: "https://collect.internal", batchSize: 200, flushInterval: 5_000, maxRetries: 5, closeTimeout: 10_000, networkTimeout: 60_000, logLevel: "debug", gzip: true, onError: (e) => console.error("[Tell]", e), }); ``` ## Tracking events Every method takes `userId` as its first parameter. For details on each event type and when to use them, see [Events & Properties](/tracking/events-and-properties). ```typescript theme={null} tell.track("user_123", "Feature Used", { feature: "dark_mode", enabled: true }); tell.identify("user_123", { name: "Jane", email: "jane@example.com" }); tell.group("user_123", "company_456", { plan: "enterprise" }); tell.revenue("user_123", 49.99, "USD", "order_789", { product: "annual_plan" }); tell.alias("anon_visitor_abc", "user_123"); ``` Pass `undefined` when you don't need properties: ```typescript theme={null} tell.track("user_123", "App Started"); ``` ### Standard event names ```typescript theme={null} import { Events } from "@tell-rs/node"; tell.track("user_123", Events.PageViewed, { url: "/pricing" }); tell.track("user_123", Events.UserSignedUp, { source: "organic" }); tell.track("user_123", Events.OrderCompleted, { total: 99.00 }); tell.track("user_123", Events.FeatureUsed, { feature: "export" }); ``` Constants cover user lifecycle, revenue, subscriptions, trials, shopping, engagement, and communication events. Custom string names always work too. ### Super properties ```typescript theme={null} tell.register({ app_version: "2.1.0", env: "production" }); // This track call automatically includes app_version and env tell.track("user_123", "Click", { button: "submit" }); tell.unregister("env"); ``` Event-specific properties override super properties when keys conflict. See [Events & Properties](/tracking/events-and-properties) for more on property patterns. ## Structured logging ```typescript theme={null} tell.logError("Payment failed", "billing", { error: "card_declined", amount: 9.99 }); tell.logInfo("User signed in", "auth", { method: "oauth" }); tell.logWarning("High memory usage", "api"); ``` The `service` parameter defaults to `"app"`. Nine severity levels from `logEmergency` to `logTrace`. See [Logs](/tracking/logs) for the full level reference and when to use each. Generic logging method: ```typescript theme={null} tell.log("error", "Something broke", "worker", { job_id: "j_123" }); ``` ## Redaction & beforeSend Transform or drop events before they're queued. Return `null` to drop an item. Supports a single function or an array of functions applied in order. ### Drop health-check events ```typescript theme={null} const tell = new Tell({ apiKey: "YOUR_API_KEY", beforeSend: (event) => { if (event.properties?.url === "/health") return null; return event; }, }); ``` ### Scrub user PII from traits ```typescript theme={null} const tell = new Tell({ apiKey: "YOUR_API_KEY", beforeSend: (event) => { if (event.traits?.email) { return { ...event, traits: { ...event.traits, email: "[REDACTED]" } }; } return event; }, beforeSendLog: [scrubPII, addTimezone], // pipeline of functions }); ``` ### `redact()` utility For common patterns, use the built-in `redact()` factory: ```typescript theme={null} import { Tell, redact, redactLog, SENSITIVE_PARAMS } from "@tell-rs/node"; const tell = new Tell({ apiKey: "YOUR_API_KEY", beforeSend: redact({ dropRoutes: ["/health", "/readyz"], stripParams: SENSITIVE_PARAMS, redactKeys: ["email", "phone", "ssn"], }), beforeSendLog: redactLog({ redactKeys: ["password", "credit_card"], }), }); ``` You can combine `redact()` with custom hooks in an array: ```typescript theme={null} const tell = new Tell({ apiKey: "YOUR_API_KEY", beforeSend: [ redact({ stripParams: SENSITIVE_PARAMS }), myCustomHook, ], }); ``` Server-side redaction prevents sensitive data from ever leaving your infrastructure. For defense in depth, also configure [pipeline redaction](/pipeline/transforms/redact) to catch anything that slips through. ## Lifecycle ```typescript theme={null} await tell.flush(); // Force-send all queued events tell.resetSession(); // Rotate session ID await tell.close(); // Flush + shut down ``` Always call `close()` before your process exits to avoid losing buffered events. ## Error handling Tracking calls (`track`, `identify`, `group`, `revenue`, `alias`) and logging calls never throw. Validation errors and network failures route through the `onError` callback: ```typescript theme={null} const tell = new Tell({ apiKey: "YOUR_API_KEY", onError: (err) => console.error("[Tell]", err.message), }); ``` Constructor and lifecycle methods (`flush`, `close`) can throw or reject — handle these in your application code. ## Retry behavior On HTTP send failure (5xx or network error), the SDK retries with exponential backoff: 1-second base delay, 1.5x multiplier, 20% jitter, capped at 30 seconds. After `maxRetries` attempts (default: 3), the batch is dropped and reported via `onError`. 4xx errors (except 413) are not retried — they indicate a client-side issue (e.g. invalid API key). On 413 (payload too large), the SDK automatically halves the batch size for future sends. ## Configuration reference | Parameter | Production | Development | Description | | - | - | - | - | | `endpoint` | `https://collect.tell.app` | `http://localhost:8080` | HTTP collector URL | | `batchSize` | 100 | 10 | Events per batch before auto-flush | | `flushInterval` | 10,000 ms | 2,000 ms | Time between auto-flushes | | `maxRetries` | 3 | 3 | Retry attempts on send failure | | `closeTimeout` | 5,000 ms | 5,000 ms | Max wait on `close()` | | `networkTimeout` | 30,000 ms | 30,000 ms | HTTP request timeout | | `maxQueueSize` | 1,000 | 1,000 | Max queued items before oldest drops | | `gzip` | false | false | Compress payloads with gzip | | `source` | `os.hostname()` | `os.hostname()` | Log source identifier | | `logLevel` | `error` | `debug` | SDK log verbosity | | `disabled` | false | false | Drop all events locally | # React SDK Source: https://docs.tell.rs/tracking/sdks/javascript/react React provider and hooks for Tell analytics. ```bash theme={null} npm install @tell-rs/react @tell-rs/browser ``` Wrap your app with `TellProvider`, then use hooks for tracking. `@tell-rs/react` is a thin wrapper around the [browser SDK](/tracking/sdks/javascript/browser). It connects Tell to React's component lifecycle — the provider calls `configure()` on mount and `close()` on unmount. You install `@tell-rs/browser` alongside because that's where the actual tracking, sessions, and transport live. ## Setup ```tsx theme={null} import { TellProvider } from "@tell-rs/react"; function App() { return ( ); } ``` ### With redaction ```tsx theme={null} import { TellProvider } from "@tell-rs/react"; import { redact, SENSITIVE_PARAMS } from "@tell-rs/browser"; function App() { return ( ); } ``` ## Hooks ```tsx theme={null} import { useTell, useTrack, useIdentify } from "@tell-rs/react"; function SignUpButton() { const track = useTrack(); const identify = useIdentify(); function handleSignUp(user) { identify(user.id, { name: user.name }); track("Sign Up Completed", { plan: "pro" }); } return ; } ``` | Hook | Returns | Notes | | - | - | - | | `useTell()` | Full browser SDK instance | For advanced use cases | | `useTrack()` | `(event, properties?) => void` | Stable ref, safe for dependency arrays | | `useIdentify()` | `(userId, traits?) => void` | Stable ref, safe for dependency arrays | ## Direct access For code outside of React components (utilities, background tasks): ```typescript theme={null} import { tell } from "@tell-rs/react"; tell.track("Background Task"); ``` ## What you get from the browser SDK All features documented in the [browser SDK guide](/tracking/sdks/javascript/browser) are available — sessions, device context, super properties, structured logging, opt-out, and `beforeSend` hooks. The React package only adds lifecycle integration and hooks. # TanStack Router Source: https://docs.tell.rs/tracking/sdks/javascript/tanstack Page view tracking for TanStack Router (Start, SPA) apps. ```bash theme={null} npm install @tell-rs/react @tell-rs/browser ``` Use `@tell-rs/react` with a small page tracker component that watches TanStack Router's `useLocation()`. There is no separate `@tell-rs/tanstack` package. TanStack Router integration uses the [React SDK](/tracking/sdks/javascript/react) directly — you just add a `` component that listens for route changes. ## Setup ### 1. Add the provider and page tracker ```tsx theme={null} // src/routes/__root.tsx import { createRootRoute, Outlet, useLocation } from "@tanstack/react-router"; import { TellProvider, useTell } from "@tell-rs/react"; import { useEffect, type ReactNode } from "react"; function TellPageTracker() { const tell = useTell(); const location = useLocation(); useEffect(() => { tell.track("Page Viewed", { url: window.location.href, path: location.pathname, referrer: document.referrer, title: document.title, }); }, [location.pathname, tell]); return null; } export const Route = createRootRoute({ component: RootComponent, }); function RootComponent() { return ( ); } ``` `TellPageTracker` fires a `Page Viewed` event on mount and whenever `location.pathname` changes. Place it inside `TellProvider` so `useTell()` resolves. ### 2. Track events ```tsx theme={null} import { useTrack } from "@tell-rs/react"; function SignUpButton() { const track = useTrack(); return ( ); } ``` ### 3. Identify users ```tsx theme={null} import { useIdentify } from "@tell-rs/react"; function LoginForm() { const identify = useIdentify(); function onLogin(user: { id: string; email: string }) { identify(user.id, { email: user.email }); } // ... } ``` ## With TanStack Start (SSR) TanStack Start renders on the server first. The browser SDK requires `window` and `document`, so tracking only runs client-side. This works automatically — `TellProvider` calls `configure()` inside a `useEffect`, which only fires in the browser. For server-side tracking in API routes or server functions, use the [Node.js SDK](/tracking/sdks/javascript/node): ```typescript theme={null} import { Tell, production } from "@tell-rs/node"; const tell = new Tell(production("YOUR_API_KEY")); // In a server function tell.track("user_123", "Order Completed", { total: 99.99 }); ``` ## With redaction ```tsx theme={null} import { TellProvider } from "@tell-rs/react"; import { redact, SENSITIVE_PARAMS } from "@tell-rs/browser"; function RootComponent() { return ( ); } ``` ## What you get from the browser SDK All features documented in the [browser SDK guide](/tracking/sdks/javascript/browser) are available — sessions, device context, super properties, structured logging, opt-out, and `beforeSend` hooks. The page tracker component only adds route-change awareness. # Vue SDK Source: https://docs.tell.rs/tracking/sdks/javascript/vue Vue plugin and composable for Tell analytics. ```bash theme={null} npm install @tell-rs/vue @tell-rs/browser ``` A plugin with a composable and global `$tell` property. `@tell-rs/vue` is a thin wrapper around the [browser SDK](/tracking/sdks/javascript/browser). It connects Tell to Vue's app lifecycle — the plugin calls `configure()` on install and `close()` on app unmount. You install `@tell-rs/browser` alongside because that's where the actual tracking, sessions, and transport live. ## Setup ```typescript theme={null} import { createApp } from "vue"; import { TellPlugin } from "@tell-rs/vue"; const app = createApp(App); app.use(TellPlugin, { apiKey: "YOUR_API_KEY" }); app.mount("#app"); ``` ### With options ```typescript theme={null} import { redact, SENSITIVE_PARAMS } from "@tell-rs/browser"; app.use(TellPlugin, { apiKey: "YOUR_API_KEY", options: { beforeSend: redact({ stripParams: SENSITIVE_PARAMS }), captureErrors: true, }, }); ``` ## Composable ```vue theme={null} ``` ## Page view tracking with Vue Router ```typescript theme={null} import { tell } from "@tell-rs/vue"; import router from "./router"; router.afterEach((to) => { tell.track("Page Viewed", { path: to.path, url: to.fullPath }); }); ``` ## What you get from the browser SDK All features documented in the [browser SDK guide](/tracking/sdks/javascript/browser) are available — sessions, device context, super properties, structured logging, opt-out, and `beforeSend` hooks. The Vue package only adds lifecycle integration and the composable. # Rust SDK Source: https://docs.tell.rs/tracking/sdks/rust Track events and logs from Rust services with sub-microsecond overhead. The Rust SDK sends events and structured logs from your backend services to Tell. It's a server SDK — you create a client, pass a user ID on every call, and a background worker handles batching and delivery over TCP. Your thread does about 80 ns of work per call (serialize, encode, enqueue) and never touches the network. ## Installation ```bash theme={null} cargo add tell cargo add tokio --features rt-multi-thread,macros ``` The SDK requires Rust 2024 edition and a Tokio runtime. ## Quick start ```rust theme={null} use tell::{Tell, TellConfig, props}; #[tokio::main] async fn main() { let client = Tell::new( TellConfig::production("a1b2c3d4e5f60718293a4b5c6d7e8f90").unwrap() ).unwrap(); client.track("user_123", "Page Viewed", props! { "url" => "/home", "referrer" => "google" }); client.close().await.ok(); } ``` That's a working setup. The client connects to `collect.tell.rs:50000`, batches up to 100 events, and flushes every 10 seconds or when you call `close()`. ## Configuration Use one of the two presets, or build a custom config. ```rust theme={null} // Production — collect.tell.rs:50000, batch 100, flush 10s let config = TellConfig::production("a1b2c3d4e5f60718293a4b5c6d7e8f90").unwrap(); // Development — localhost:50000, batch 10, flush 2s, debug logging let config = TellConfig::development("a1b2c3d4e5f60718293a4b5c6d7e8f90").unwrap(); ``` For custom settings, use the builder: ```rust theme={null} use std::time::Duration; let config = TellConfig::builder("a1b2c3d4e5f60718293a4b5c6d7e8f90") .endpoint("collect.internal:50000") .batch_size(200) .flush_interval(Duration::from_secs(5)) .max_retries(5) .close_timeout(Duration::from_secs(10)) .network_timeout(Duration::from_secs(15)) .on_error(|e| eprintln!("[Tell] {e}")) .build() .unwrap(); ``` The API key must be a 32-character hex string. The SDK validates it at config time and returns an error if it's invalid. ## Tracking events Every tracking method takes `user_id` as its first parameter. Calls never block or panic — errors go to the optional `on_error` callback. ```rust theme={null} // Track a custom event client.track("user_123", "Feature Used", props! { "feature" => "dark_mode", "enabled" => true }); // Identify a user client.identify("user_123", props! { "name" => "Jane", "plan" => "pro", "company" => "Acme" }); // Associate user with a group client.group("user_123", "company_456", props! { "plan" => "enterprise", "seats" => 50 }); // Track revenue client.revenue("user_123", 49.99, "USD", "order_789", props! { "product" => "annual_plan" }); // Link two user identities client.alias("anon_abc", "user_123"); ``` ### Standard event names The SDK provides typed constants for common events so you don't have to remember exact strings: ```rust theme={null} use tell::Events; client.track("user_123", Events::PAGE_VIEWED, props! { "url" => "/pricing" }); client.track("user_123", Events::USER_SIGNED_UP, props! { "source" => "organic" }); client.track("user_123", Events::ORDER_COMPLETED, props! { "total" => 99.00 }); client.track("user_123", Events::FEATURE_USED, props! { "feature" => "export" }); ``` Constants are available for user lifecycle, revenue, subscriptions, trials, shopping, engagement, and communication events. Custom string names are always accepted too. ### Super properties Register properties that get merged into every `track`, `group`, and `revenue` call: ```rust theme={null} client.register(props! { "app_version" => "2.1.0", "env" => "production" }); // This track call automatically includes app_version and env client.track("user_123", "Click", props! { "button" => "submit" }); // Remove a super property client.unregister("env"); ``` Event-specific properties override super properties when keys conflict. ## Structured logging Send logs alongside events through the same pipeline. Each log has an RFC 5424 severity level. ```rust theme={null} client.log_error("DB connection failed", Some("api"), props! { "host" => "db.internal", "retries" => 3 }); client.log_info("Request processed", Some("api"), props! { "status" => 200, "duration_ms" => 45 }); client.log_warning("Rate limit approaching", Some("gateway"), None::); ``` The `service` parameter is optional — pass `None` to default to `"app"`. Convenience methods are available for all nine levels: `log_emergency`, `log_alert`, `log_critical`, `log_error`, `log_warning`, `log_notice`, `log_info`, `log_debug`, and `log_trace`. You can also use the generic `log` method with an explicit level: ```rust theme={null} use tell_encoding::LogLevel; client.log(LogLevel::Error, "Something broke", Some("worker"), props! { "job_id" => "j_123" }); ``` ## Properties You have three ways to pass properties: ```rust theme={null} use tell::{props, Props}; // 1. props! macro — fastest, zero intermediate allocation client.track("user_123", "Click", props! { "url" => "/home", "count" => 42, "active" => true }); // 2. Props builder — for dynamic values let p = Props::new() .add("url", &request.path) .add("status", response.status); client.track("user_123", "Request", p); // 3. serde_json — works with any Serialize type client.track("user_123", "Click", Some(json!({"url": "/home"}))); // 4. No properties client.track("user_123", "Click", None::); ``` `Props` and `props!` write JSON bytes directly to a buffer, skipping the intermediate `serde_json::Value` allocation. Use them on hot paths. ## Lifecycle ```rust theme={null} // Force-send all queued events and logs client.flush().await?; // Rotate session ID client.reset_session(); // Flush + shut down the background worker client.close().await?; ``` Always call `close()` before your process exits to avoid losing buffered events. It blocks up to `close_timeout` (default 5 seconds). ## Sharing across threads `Tell` is `Clone + Send + Sync`. Internally it wraps everything in an `Arc`, so cloning is cheap: ```rust theme={null} let client = Tell::new(config).unwrap(); let c = client.clone(); tokio::spawn(async move { c.track("user_456", "Background Job", props! { "job" => "sync" }); }); ``` ## Error handling The constructor and lifecycle methods return `Result`: ```rust theme={null} let client = Tell::new(config)?; // TellError::Configuration if invalid client.flush().await?; // TellError::Network on failure client.close().await?; // TellError::Closed if already shut down ``` Tracking and logging calls (`track`, `identify`, `log_*`, etc.) never return errors. Invalid input (empty user ID, event name too long) is reported through the `on_error` callback: ```rust theme={null} let config = TellConfig::builder("a1b2c3d4e5f60718293a4b5c6d7e8f90") .on_error(|e| eprintln!("[Tell] {e}")) .build() .unwrap(); ``` ## Advanced ### Configuration reference | Parameter | Default (production) | Default (development) | Description | | - | - | - | - | | `endpoint` | `collect.tell.rs:50000` | `localhost:50000` | TCP collector address | | `batch_size` | 100 | 10 | Events per batch before auto-flush | | `flush_interval` | 10s | 2s | Time between auto-flushes | | `max_retries` | 3 | 3 | Retry attempts on send failure | | `close_timeout` | 5s | 5s | Max wait on `close()` | | `network_timeout` | 30s | 30s | TCP connect timeout | | `on_error` | silent | silent | Error callback | ### Validation rules | Field | Rule | | - | - | | API key | 32-character hex string | | User ID | Non-empty | | Event name | 1–256 characters | | Log message | 1–65,536 characters | | Group ID | Non-empty | | Revenue amount | Positive number | ### Retry behavior On TCP send failure, the SDK retries with exponential backoff: 1s initial delay, 1.5x multiplier, 20% jitter, capped at 30s. After `max_retries` attempts, the batch is dropped and the error reported via `on_error`. # Swift SDK Source: https://docs.tell.rs/tracking/sdks/swift Track events and logs from iOS, macOS, and visionOS apps. The Swift SDK tracks events and structured logs from your Apple apps. It's a client SDK — you configure a singleton once at launch, and it handles device IDs, sessions, and delivery automatically. Events called before initialization completes are queued and replayed. Your app never blocks on network I/O. ## Installation Add the package in Xcode: **File > Add Package Dependencies** and enter: ``` https://github.com/tell-rs/tell-swift.git ``` Or in your `Package.swift`: ```swift theme={null} dependencies: [ .package(url: "https://github.com/tell-rs/tell-swift.git", from: "1.0.0") ] ``` Requires iOS 16.0+, macOS 13.0+, or visionOS 1.0+, with Swift 6.0+ and Xcode 16.0+. ## Quick start ```swift theme={null} import Tell // Configure once at app startup Tell.shared.configure(apiKey: "a1b2c3d4e5f60718293a4b5c6d7e8f90", preset: .production) // Track events Tell.shared.track(.pageViewed, properties: ["url": "/home"]) // Identify users Tell.shared.identify("user_123", traits: ["name": "Jane", "plan": "pro"]) ``` That's it. The SDK generates a device ID, starts a session, collects device context, and batches events over TCP. Events fired before `configure` completes are queued (up to 1,000) and sent once ready. ## Configuration Use a preset for one-line setup, or pass individual parameters. ```swift theme={null} // Production — collect.tell.rs:50000, batch 100, flush 10s Tell.shared.configure(apiKey: "YOUR_API_KEY", preset: .production) // Development — localhost:50000, batch 10, flush 2s Tell.shared.configure(apiKey: "YOUR_API_KEY", preset: .development) ``` For fire-and-forget initialization (returns immediately, connects in the background): ```swift theme={null} Tell.shared.configureAsync( apiKey: "YOUR_API_KEY", endpoint: "collect.tell.rs:50000", batchSize: 100, flushInterval: 10, logLevel: .debug ) ``` For async initialization with error handling: ```swift theme={null} try await Tell.shared.configure( apiKey: "YOUR_API_KEY", sessionTimeout: 30 * 60, defaultOptOut: false, onError: { error in print("Tell: \(error)") } ) ``` The API key must be a 32-character hex string. ## Tracking events No user ID needed — the SDK tracks the current user automatically (anonymous until you call `identify`). ```swift theme={null} // Typed event names Tell.shared.track(.featureUsed, properties: [ "feature": "dark_mode", "enabled": true ]) // String event names Tell.shared.track("Custom Event", properties: ["key": "value"]) // No properties Tell.shared.track(.pageViewed) ``` ### Standard event names The SDK provides strongly typed constants for common events: ```swift theme={null} Tell.shared.track(.pageViewed, properties: ["url": "/pricing"]) Tell.shared.track(.userSignedUp, properties: ["source": "organic"]) Tell.shared.track(.orderCompleted, properties: ["total": 99.00]) Tell.shared.track(.featureUsed, properties: ["feature": "export"]) Tell.shared.track(.subscriptionStarted, properties: ["plan": "pro"]) ``` Constants cover user lifecycle, revenue, subscriptions, trials, shopping, engagement, communication, and session events. Custom string names always work too. ### Revenue Track purchases with typed currency codes: ```swift theme={null} Tell.shared.revenue( amount: 49.99, currency: .usd, orderID: "order_456", properties: ["product": "annual_plan"] ) ``` The SDK includes constants for 60+ currencies (`.usd`, `.eur`, `.gbp`, `.jpy`, `.btc`, etc.) and accepts custom currency strings via `Currency("XYZ")`. ### User identity ```swift theme={null} // Identify a known user — stores the ID for all subsequent events Tell.shared.identify("user_123", traits: [ "name": "Jane", "email": "jane@example.com", "plan": "pro" ]) // Associate user with a group Tell.shared.group("company_456", properties: [ "name": "Acme Corp", "plan": "enterprise" ]) // Link two identities Tell.shared.alias("anon_visitor_abc", userId: "user_123") ``` ### Super properties Register properties that get merged into every `track`, `group`, and `revenue` call: ```swift theme={null} Tell.shared.register(["app_version": "2.1.0", "env": "production"]) // This track call automatically includes app_version and env Tell.shared.track(.featureUsed, properties: ["feature": "export"]) // Remove a super property Tell.shared.unregister("env") ``` Event-specific properties override super properties when keys conflict. ## Structured logging Send logs alongside events through the same pipeline: ```swift theme={null} Tell.shared.logError("Payment failed", service: "billing", data: [ "error": "card_declined", "amount": 9.99 ]) Tell.shared.logInfo("User signed in", service: "auth", data: [ "method": "oauth" ]) Tell.shared.logWarning("High memory usage", service: "app") ``` The `service` parameter defaults to `"app"`. Convenience methods are available for all nine RFC 5424 levels: `logEmergency`, `logAlert`, `logCritical`, `logError`, `logWarning`, `logNotice`, `logInfo`, `logDebug`, and `logTrace`. ## Sessions Sessions are managed automatically. The SDK: 1. Generates a persistent device ID (stored in UserDefaults) 2. Creates a new session on launch with a device context snapshot 3. Tracks app background/foreground transitions 4. Starts a new session if the app was backgrounded longer than `sessionTimeout` (default: 30 minutes) Device context collected automatically includes device model, OS version, app version, screen dimensions, locale, timezone, and battery state (iOS). ## Privacy ```swift theme={null} // Stop all data collection — events are dropped locally Tell.shared.optOut() // Resume data collection Tell.shared.optIn() // Check current state if Tell.shared.isOptedOut() { // Show consent prompt } ``` Opt-out state is persisted across sessions. For privacy-first apps, set `defaultOptOut: true` during configuration so new users start opted out. When opted out, all tracking and logging calls silently drop. `flush()` and `close()` still work. ## Lifecycle ```swift theme={null} // Force-send all queued events try await Tell.shared.flush() // Reset user — generates new anonymous ID, clears super properties Tell.shared.reset() // Graceful shutdown try await Tell.shared.close() ``` Call `reset()` on logout to start a fresh anonymous session for the next user. ## Advanced ### Configuration reference | Parameter | Default | Description | | - | - | - | | `endpoint` | `collect.tell.rs:50000` | TCP collector address | | `batchSize` | 100 | Events per batch before auto-flush | | `flushInterval` | 10s | Time between auto-flushes | | `sessionTimeout` | 30 min | Background time before new session | | `defaultOptOut` | false | Initial opt-out state for new users | | `generateEventIds` | false | Client-side event deduplication IDs | | `logLevel` | `.info` | SDK internal log verbosity | ### Validation rules | Field | Rule | | - | - | | API key | 32-character hex string | | Event name | 1–256 characters | | Log message | 1–65,536 characters | | User ID | Non-empty | | Group ID | Non-empty | | Order ID | Non-empty | | Revenue amount | Positive number | ### Pre-initialization queue Events called before `configure` completes are buffered (up to 1,000 items). On overflow, the oldest event is dropped. Once initialization succeeds, all queued events are replayed in order. # Sessions Source: https://docs.tell.rs/tracking/sessions How Tell groups events into sessions and what session data is available. Sessions group a user's events into a single visit. Every event carries a session ID, so you can answer questions like "how many sessions included a purchase" or "what did this user do in their last session." ## How sessions work A session starts when the SDK initializes (app launch, first page load) and ends when the user is inactive for longer than the **session timeout** (30 minutes by default) or exceeds the **max session length** (24 hours by default). On web, the session ID is persisted to localStorage so page navigations continue the same session. Every event sent during a session shares the same 16-byte session ID. When a session expires and the user returns, a new session ID is generated and all subsequent events are grouped under it. Sessions are tracked alongside a **device ID** — a persistent identifier for the physical device. The device ID stays the same across sessions; the session ID rotates. ## Session lifecycle On mobile (iOS, macOS, visionOS), the SDK observes app lifecycle events and sends **context events** at each transition: | Trigger | Context event | What happens | | - | - | - | | App launch | `session_started` | New session ID generated, full device context collected | | App backgrounded | `app_backgrounded` | Timestamp recorded, context event sent | | App foregrounded | `app_foregrounded` | If background time \< timeout, session continues | | Background > timeout | `session_timeout` | New session ID generated, context event sent | | App terminated | `app_terminated` | Final context event sent | Active/inactive transitions (e.g. pulling down notification center) update internal state but don't generate context events — they happen too frequently to be useful. ### Session timeout The default timeout is 30 minutes. If a user backgrounds your app and returns within 30 minutes, the same session continues. If they return after 30 minutes, a new session starts. You can configure this at initialization: ```swift theme={null} try await Tell.shared.configure( apiKey: "YOUR_API_KEY", sessionTimeout: 15 * 60 // 15 minutes ) ``` ```javascript theme={null} tell.configure('YOUR_API_KEY', { sessionTimeout: 15 * 60 * 1000 }); // 15 minutes ``` ### Max session length Sessions are capped at 24 hours by default to prevent zombie sessions from tabs left open indefinitely. When the max length is reached, the session rotates even if the user is still active. ```javascript theme={null} tell.configure('YOUR_API_KEY', { maxSessionLength: 12 * 60 * 60 * 1000 }); // 12 hours ``` ## Context data Each context event carries a snapshot of the device and app environment. On mobile SDKs, this is collected automatically — you don't need to send it yourself. ### Automatically collected properties **Device** | Property | Example | Description | | - | - | - | | `device_type` | `mobile`, `tablet`, `desktop`, `tv`, `watch`, `vr` | Device form factor | | `device_model` | `iPhone`, `Mac14,2` | Hardware model | | `operating_system` | `ios`, `macos`, `visionos` | OS family | | `os_version` | `18.2.0` | Semver OS version | **App** | Property | Example | Description | | - | - | - | | `app_version` | `2.1.0` | Bundle short version string | | `app_build` | `42` | Build number | | `app_bundle_id` | `com.example.app` | Bundle identifier | **Screen** | Property | Example | Description | | - | - | - | | `screen_width` | `1170` | Physical pixel width | | `screen_height` | `2532` | Physical pixel height | | `screen_scale` | `3.0` | Display scale factor | | `screen_logical_width` | `390` | Logical point width | | `screen_logical_height` | `844` | Logical point height | **System** | Property | Example | Description | | - | - | - | | `memory_total` | `6442450944` | Total physical memory (bytes) | | `memory_available` | `104857600` | Resident memory usage (bytes) | | `storage_available` | `48318382080` | Available disk space (bytes) | | `storage_total` | `127989493760` | Total disk capacity (bytes) | | `battery_level` | `0.85` | Battery charge 0.0–1.0 (iOS only) | | `battery_state` | `charging` | `unplugged`, `charging`, or `full` (iOS only) | **Locale** | Property | Example | Description | | - | - | - | | `locale` | `en_US` | Device locale | | `timezone` | `America/New_York` | Device timezone | **Session** | Property | Example | Description | | - | - | - | | `session_reason` | `session_start` | Why this context event was sent | | `app_state` | `active` | App state at time of event | | `background_duration` | `1847.5` | Seconds spent in background (if returning) | ### Adding custom context Use super properties to attach custom context to every event: ```javascript theme={null} tell.register({ deployment: 'canary', app_version: '2.1.0', build_sha: 'a1b2c3d' }); ``` On the server side, pass context as event properties. There's no fixed schema — the properties field accepts any key-value data, so you can enrich events with whatever is relevant to your application. ## Device IDs The device ID is a 16-byte UUID that persists across sessions and app updates. | Platform | Storage | Lifetime | | - | - | - | | iOS / visionOS | Keychain | Survives app reinstalls | | macOS | Keychain | Survives app reinstalls | | Web | `localStorage` | Per browser, cleared on storage wipe | | Server | Not applicable | Use user IDs directly | Mobile SDKs generate and store the device ID automatically. You never need to manage it yourself. ## What gets attached to events Every event (track, identify, group, revenue, alias) automatically gets: 1. **Device ID** — from the session manager 2. **Session ID** — from the session manager 3. **Minimal device context** — `device_type`, `operating_system`, `os_version`, `app_version` (if device context enrichment is enabled) 4. **Timestamp** — current time unless overridden You don't need to pass any of this manually. The SDK handles it. ## Manual session control You can force a new session at any time: ```swift theme={null} // Swift — starts new session with custom reason await Tell.shared.sessionManager?.startNewSession(reason: "user_switched") ``` Call `reset()` to clear the current user, generate a new anonymous ID, and stop the current session. The next event starts a fresh session. ```swift theme={null} Tell.shared.reset() ``` ## What's next * [Users & Identity](/tracking/users-and-identity) — device IDs, user IDs, and identity merging * [Events & Properties](/tracking/events-and-properties) — the event model * [SDKs](/tracking/sdks/swift) — platform-specific integration guides # Users & Identity Source: https://docs.tell.rs/tracking/users-and-identity How Tell identifies users — device IDs, user IDs, and identity merging. Tell tracks users through three layers of identity: **device IDs** (automatic), **anonymous IDs** (automatic), and **user IDs** (you provide these). Together they let you follow a user from their first anonymous visit through login and across devices. ## Identity layers ### Device ID A 16-byte UUID generated the first time the SDK initializes on a device. On mobile, it's stored in the Keychain and survives app reinstalls. On web, it lives in `localStorage`. You never set this yourself — the SDK creates and manages it. Every event includes the device ID automatically. ### Anonymous ID A UUID string assigned to unidentified users. The SDK generates one on first launch and persists it in UserDefaults (mobile) or `localStorage` (web). All events use this as the user ID until you call `identify`. The anonymous ID is regenerated when you call `reset()` (e.g. on logout), giving the next user a clean slate. ### User ID The identifier you provide when you know who the user is — typically after login or signup. Once set, all subsequent events use this user ID instead of the anonymous ID. ```swift theme={null} Tell.shared.identify("user_123", traits: [ "name": "Jane", "email": "jane@example.com", "plan": "pro" ]) ``` ```javascript theme={null} tell.identify('user_123', { name: 'Jane', email: 'jane@example.com', plan: 'pro' }); ``` The second argument is **traits** — persistent properties associated with the user. Traits are stored with the identify event and appear in user analytics. ## How identification works Before `identify` is called, events are sent with the anonymous ID: ``` Event: page_view user_id: "550e8400-e29b-..." (anonymous) Event: feature_used user_id: "550e8400-e29b-..." (anonymous) ``` After `identify("user_123")`, the SDK switches to the known user ID: ``` Event: identify user_id: "user_123" traits: {name: "Jane", ...} Event: purchase user_id: "user_123" Event: page_view user_id: "user_123" ``` The user ID persists across sessions until `reset()` is called. ## Identity resolution with alias When you discover that two IDs belong to the same person — for example, an anonymous visitor who later logs in on a different device — use `alias` to merge them: ```swift theme={null} Tell.shared.alias("anon_visitor_abc", userId: "user_123") ``` ```javascript theme={null} tell.alias('anon_visitor_abc', 'user_123'); ``` This tells Tell that the previous ID (`anon_visitor_abc`) and the new user ID (`user_123`) are the same person. Historical events from the previous ID can then be attributed to the merged identity. ## Groups Associate a user with a company, team, or organization: ```swift theme={null} Tell.shared.group("company_456", properties: [ "name": "Acme Corp", "plan": "enterprise", "employees": 150 ]) ``` ```javascript theme={null} tell.group('company_456', { name: 'Acme Corp', plan: 'enterprise', employees: 150 }); ``` Group properties work like user traits — they're stored with the group event and can be used in analytics to break down metrics by company or team. ## Reset (logout) When a user logs out, call `reset()` to: * Clear the current user ID (reverts to anonymous) * Generate a new anonymous ID * Clear all super properties * Reset opt-out state to the configured default * Stop the current session (next event starts a fresh one) ```swift theme={null} Tell.shared.reset() ``` ```javascript theme={null} tell.reset(); ``` This ensures the next user on a shared device gets a clean identity. ## Event types for identity | Method | Event type | When to use | | - | - | - | | `identify` | `identify` | User logs in or signs up — link device to known user | | `alias` | `alias` | Merge two identities (anonymous to known, or cross-device) | | `group` | `group` | Associate user with a company or team | | `reset` | — | User logs out — clear identity for next user | ## What gets sent automatically You don't need to pass identity information on every event. The SDK tracks: * **Device ID** — generated once, attached to every event * **Session ID** — rotated on timeout, attached to every event * **User ID** — anonymous until `identify`, then the known ID On mobile, the SDK also attaches device context (device type, OS, app version) to context events automatically. See [Sessions](/tracking/sessions) for the full list of collected properties. ## What's next * [Sessions](/tracking/sessions) — session lifecycle and device context * [Events & Properties](/tracking/events-and-properties) — the event model and property patterns * [SDKs](/tracking/sdks/swift) — platform-specific integration guides