This is the full developer documentation for WireLog # MCP Server > Connect WireLog to AI agents via the Model Context Protocol. TypeScript MCP server that exposes WireLog as native tools for AI agents. Uses stdio transport. **GitHub:** [github.com/wirelogai/wirelog-mcp](https://github.com/wirelogai/wirelog-mcp) **Package:** `@wirelogai/mcp` on npm ## Install ```bash npm install -g @wirelogai/mcp ``` Or run without installing: ```bash npx @wirelogai/mcp ``` ## Configuration | Variable | Required | Default | Description | | ----------------- | -------- | ------------------------ | ------------------------------------------------ | | `WIRELOG_API_KEY` | Yes | — | `sk_` or `aat_` key with `query` + `track` scope | | `WIRELOG_HOST` | No | `https://api.wirelog.ai` | API base URL | ## Claude Desktop setup Add to `claude_desktop_config.json`: ```json { "mcpServers": { "wirelog": { "command": "npx", "args": ["@wirelogai/mcp"], "env": { "WIRELOG_API_KEY": "sk_YOUR_SECRET_KEY" } } } } ``` Restart Claude Desktop. The four WireLog tools appear in the tool list. ## Tools ### wirelog\_query Run a pipe DSL query. Returns Markdown by default. | Parameter | Type | Required | Description | | --------- | ------------------------------ | -------- | ------------------------------------------ | | `q` | string | Yes | Pipe DSL query string | | `format` | `"llm"` \| `"json"` \| `"csv"` | No | Output format. Default `"llm"` (Markdown). | | `limit` | number | No | Max rows. Default 100, max 10000. | ```text Example: {"q": "signup | last 30d | count by day", "format": "llm"} ``` ### wirelog\_track Track an analytics event. | Parameter | Type | Required | Description | | ------------------ | ------ | -------- | ----------------------------------- | | `event_type` | string | Yes | Event name | | `user_id` | string | No | User identifier | | `device_id` | string | No | Device identifier | | `event_properties` | object | No | Arbitrary event properties | | `user_properties` | object | No | User properties sent with the event | ### wirelog\_identify Bind a device to a user and/or set profile properties. | Parameter | Type | Required | Description | | ------------------- | ------ | -------- | --------------------------------------------------- | | `user_id` | string | Yes | User identifier | | `device_id` | string | No | Device to bind (recommended for stitching) | | `user_properties` | object | No | Flat key-value properties (behaves like `$set`) | | `user_property_ops` | object | No | Granular ops: `$set`, `$set_once`, `$add`, `$unset` | ### wirelog\_list\_events Discover tracked event types. Takes no parameters. Runs: ```text * | last 30d | count by event_type | top 50 ``` Returns a Markdown table of event names and counts. ## Usage pattern The agent should call `wirelog_list_events` first to discover what events exist, then write targeted queries with `wirelog_query`. Do not guess event names. ```text Agent: wirelog_list_events -> | event_type | count | |-------------|-------| | page_view | 12340 | | signup | 890 | | purchase | 210 | Agent: wirelog_query {"q": "funnel signup -> purchase | last 30d"} -> | step | count | rate | |----------|-------|--------| | signup | 890 | 100.0% | | purchase | 210 | 23.6% | ``` ## Next steps * [Claude Code Skills](/agents/skills) — skill-based integration (no tool registration needed) * [Agent Patterns](/agents/patterns) — daily briefings, anomaly detection, self-tracking * [Query language](/query-language/overview) — full DSL reference # Agents > Why WireLog is built for AI agents. MCP, skills, and patterns. Dashboards are for humans. Agents cannot click buttons, read charts, or navigate UIs. They need structured text they can parse, reason over, and act on. WireLog returns Markdown tables from queries. Every analytics result is text an agent can consume directly. ## Three integration paths ### 1. MCP Server The [WireLog MCP server](/agents/mcp-server) exposes analytics as native tools via the Model Context Protocol. Agents call `wirelog_query`, `wirelog_track`, `wirelog_identify`, and `wirelog_list_events` directly. Works with Claude Desktop, Claude Code, and any MCP-compatible agent. ### 2. Claude Code Skills [SKILL.md files](/agents/skills) teach coding agents the WireLog DSL, instrumentation patterns, and project setup. The agent reads the skill on activation and gains domain knowledge — no tool registration required. Three skills: `wirelog` (query), `wirelog-instrument` (add tracking to code), `wirelog-setup` (new project setup). ### 3. Direct HTTP Any agent can `POST /query` and get Markdown back: ```bash curl -X POST https://api.wirelog.ai/query \ -H "X-API-Key: sk_YOUR_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"q": "* | last 7d | count by event_type | top 10"}' ``` Returns a Markdown table. No SDK, no client library, no dependencies. ## The agent workflow ```text 1. Discover events -> wirelog_list_events (or: * | last 30d | count by event_type | top 20) 2. Write a query -> signup | last 30d | count by day 3. Read the Markdown table -> | day | count | |------------|-------| | 2026-02-01 | 42 | | 2026-02-02 | 38 | 4. Reason over the data -> "Signups dropped 10% day-over-day" 5. Act -> Investigate, alert, recommend, or fix ``` ## Example: agent-driven funnel analysis An agent investigating signup conversion: ```text Step 1: Discover events Query: * | last 30d | count by event_type | top 20 Result: signup (1200), activate (800), purchase (200), page_view (15000), ... Step 2: Build funnel Query: funnel signup -> activate -> purchase | last 30d Result: signup 1200 -> activate 800 (66.7%) -> purchase 200 (25.0%) Step 3: Break down by platform Query: funnel signup -> activate -> purchase | last 30d | by _platform Result: web: 60% activation, mobile: 45% activation Step 4: Reason "Mobile activation is 15pp below web. The mobile onboarding flow needs investigation." Step 5: Recommend "Add mobile-specific onboarding steps. Track mobile_onboarding_step events for granular funnel visibility." ``` ## Next steps * [MCP Server](/agents/mcp-server) — install and configure the MCP server for agent tool access * [Claude Code Skills](/agents/skills) — install WireLog skills for coding agents * [Agent Patterns](/agents/patterns) — daily briefings, self-tracking, anomaly detection, and more # Agent Patterns > Analytics patterns for AI agents: daily briefings, self-tracking, anomaly detection, and automated analysis. Reusable patterns for agents that consume and produce WireLog analytics. ## Pattern 1: Morning briefing Agent runs discovery and key metric queries on a schedule, summarizes trends. ```text # Discover what happened * | last 24h | count by event_type | top 10 # Weekly signup trend signup | last 7d | count by day # Funnel health funnel signup -> activate | last 7d ``` The agent reads the Markdown tables, compares to prior periods, and produces a natural-language summary: “Signups up 12% WoW. Activation rate steady at 65%. No anomalies.” ## Pattern 2: Agent self-tracking The agent tracks its own actions as events. Useful for auditing agent behavior, measuring tool usage, and debugging failures. Track: ```json { "event_type": "agent_action", "user_id": "agent-001", "event_properties": { "action": "query_analytics", "input": "signup | last 7d | count", "output": "142 signups", "duration_ms": 340, "model": "claude-opus-4-6" } } ``` Query own performance: ```text agent_action | last 7d | count by event_properties.action agent_action | where event_properties.action = "query_analytics" | last 7d | avg event_properties.duration_ms ``` ## Pattern 3: Anomaly detection Agent compares current period to previous period and flags significant changes. Current week: ```text signup | last 7d | count by day ``` Previous week: ```text signup | from 2026-02-08 to 2026-02-15 | count by day ``` The agent compares the two tables row-by-row. If any day drops more than 20% below the same day in the prior week, it flags the anomaly and investigates: ```text signup | where _platform = "mobile" | last 7d | count by day signup | where _platform = "web" | last 7d | count by day ``` “Mobile signups dropped 35% on Tuesday. Web was flat. Investigate the mobile signup flow.” ## Pattern 4: User investigation Agent drills into a specific user’s activity timeline. ```text user "alice@acme.org" | last 30d | list ``` Returns all events for that user in chronological order. The agent reads the timeline, identifies patterns, and answers questions: “Alice signed up on Feb 1, activated on Feb 3, but hasn’t returned since Feb 10.” Drill into a company: ```text * | where user.email_domain = "acme.org" | last 30d | count by event_type * | where user.email_domain = "acme.org" | last 30d | unique distinct_id ``` ## Pattern 5: Automated funnel analysis Agent discovers events, builds funnels, and identifies conversion bottlenecks. ```text # Step 1: Discover events * | last 30d | count by event_type | top 20 # Step 2: Build the funnel funnel signup -> activate -> purchase | last 30d # Step 3: Break down by platform funnel signup -> activate -> purchase | last 30d | by _platform # Step 4: Break down by acquisition channel funnel signup -> activate -> purchase | last 30d | by user.acquisition_channel ``` The agent identifies the biggest drop-off, segments by relevant dimensions, and recommends action: “Activation is the bottleneck (66% -> 25%). Mobile users convert at half the rate of web. Prioritize mobile onboarding improvements.” ## Pattern 6: Feedback loops Agent tracks the outcomes of its own recommendations, then measures whether they worked. Track the recommendation: ```json { "event_type": "agent_recommendation", "user_id": "agent-001", "event_properties": { "recommendation": "add_mobile_onboarding", "target_metric": "mobile_activation_rate", "baseline": "32%" } } ``` Later, query whether the target metric improved: ```text funnel signup -> activate | where _platform = "mobile" | last 7d ``` Compare to the baseline. The agent closes the loop: “Mobile activation improved from 32% to 41% after onboarding changes were deployed.” Query recommendation outcomes: ```text agent_recommendation | last 30d | list ``` The agent reviews its past recommendations and their outcomes, learning which types of suggestions produce results. ## Next steps * [Agents overview](/agents/overview) — integration paths (MCP, skills, HTTP) * [MCP Server](/agents/mcp-server) — tool definitions and setup * [Query language](/query-language/overview) — full DSL reference # Claude Code Skills > Install WireLog skills for Claude Code and other coding agents. SKILL.md files teach coding agents domain-specific capabilities. The agent reads the file on activation and gains knowledge of the WireLog DSL, instrumentation patterns, or project setup — no tool registration, no API integration. **GitHub:** [github.com/wirelogai/wirelog-skills](https://github.com/wirelogai/wirelog-skills) ## Available skills ### wirelog (query) Teaches the agent the full pipe DSL: sources, stages, fields, identity semantics, and the discovery-first workflow. After activation, the agent can write WireLog queries, explain results, and iterate on analysis. Covers: * All source types (`*`, event names, `funnel`, `retention`, `paths`, `sessions`, `user`, `users`, `formula`) * All stages (`where`, `last`, `from/to`, `count`, `unique`, `sum`, `avg`, `list`, `sort`, `limit`, `top`, `by`) * All fields (`event_type`, `distinct_id`, `event_properties.KEY`, `user.KEY`, system fields) * Identity stitching (`distinct_id = coalesce(user_id, mapped_user_id, device_id)`) * Discovery-first workflow (always run `* | last 30d | count by event_type | top 20` before writing event-specific queries) ### wirelog-instrument Teaches the agent how to add event tracking and user identification to code. Covers the HTTP API, JS SDK, Python, and Node.js. Covers: * API key types (`pk_`, `sk_`, `aat_`) and where each is safe to use * `POST /track` — single events and batch * `POST /identify` — device binding, profile ops (`$set`, `$set_once`, `$add`, `$unset`) * JS SDK setup (` ``` ### 3. Add identity Replace identify/alias calls with `POST /identify`. ```bash curl -X POST https://api.wirelog.ai/identify \ -H "X-API-Key: pk_..." \ -H "Content-Type: application/json" \ -d '{ "user_id": "alice@acme.org", "device_id": "dev_abc123", "user_properties": { "email": "alice@acme.org", "plan": "pro" } }' ``` No alias chaining WireLog uses a single-hop identity model: `device_id` maps to `user_id`. There is no alias chaining (Mixpanel `alias`) or identity merging (PostHog `$identify`). Each device maps to exactly one user. This is simpler and avoids the merge ambiguity that causes data quality issues in other platforms. ### 4. Recreate key queries Map existing dashboard queries to pipe DSL equivalents. | Old platform query | WireLog equivalent | | --------------------------------- | ------------------------------------------------------------- | | Signup trend (line chart, weekly) | `signup \| last 12w \| count by week` | | Signup-to-purchase funnel | `funnel signup -> purchase \| last 30d` | | 90-day retention | `retention signup \| last 90d` | | Revenue by week | `purchase \| last 12w \| sum event_properties.amount by week` | | Users by country | `users \| count by user.country \| top 20` | | DAU | `* \| last 30d \| unique distinct_id by day` | Replace event names with your actual event names. Discover them with: ```plaintext * | last 30d | count by event_type | top 20 ``` ### 5. Run in parallel Send events to both platforms during transition: 1. Instrument dual-tracking (send to old platform + WireLog simultaneously) 2. Run queries in both systems, compare results 3. Once WireLog numbers match expectations, cut over 4. Remove old platform SDK Tip WireLog’s permissive ingest means you can send the same property shapes you already send to Mixpanel/Amplitude/PostHog. Property types are normalized server-side — no schema setup required. ## Key differences **No dashboards.** Queries return Markdown, JSON, or CSV. Your agent, script, or notebook is the consumer. There is no visual query builder or chart renderer. **No visual query builder.** The pipe DSL is text-based. Queries are written as `source | stage | stage`. LLMs and agents can compose them directly. **No user aliases.** `device_id` maps to `user_id` in one hop. No alias chaining, no merge rules, no identity graph. Simpler model, fewer data quality surprises. **Permissive ingest.** Property types are normalized server-side. Send numbers as strings, booleans as integers — it all works. No schema enforcement, no type errors on ingest. **No group analytics (yet).** Use `user.email_domain` or `user.company_id` for B2B company-level segmentation. See [SaaS Metrics](/guides/saas-metrics#b2b-segmentation) for recipes. **Output is for machines.** Default output format is Markdown (optimized for LLM consumption). Also supports JSON and CSV. No charts, no embeds, no iframes. ## Pricing comparison | Volume | WireLog | PostHog | Amplitude | Mixpanel | | ---------- | ------------------ | ---------- | -------------- | --------------- | | 10M/month | **$0** (free tier) | \~$500 | \~$49-200 | \~$2,500 | | 50M/month | **$200** | \~$1,500 | \~$500-1,500 | \~$5,000+ | | 100M/month | **$450** | \~$2,500 | \~$1,500-3,000 | \~$5,000-10,000 | | 1B/month | **$4,950** | \~$15,000+ | Contact sales | Contact sales | WireLog: **$5 per million events. 10M free.** No per-seat pricing. No MTU math. No sales calls. * Mixpanel: \~$280/million events * Amplitude: MTU-based, opaque at scale * PostHog: \~$25/million events (at volume) * WireLog: $5/million events See [Pricing](/reference/pricing) for details. # SaaS Metrics > Query recipes for funnels, retention, conversion, revenue, and segmentation. Discover your event names first All event names below are **placeholders**. Your project has its own event names. Run this before anything else: ```plaintext * | last 30d | count by event_type | top 20 ``` Replace ``, ``, ``, etc. with real values from your project. ## Acquisition ### Weekly signups ```plaintext | last 12w | count by week ``` One row per week with signup count. 12-week window gives a quarter of trend data. ### Signups by channel ```plaintext | last 30d | count by user.acquisition_channel ``` Requires `acquisition_channel` set via `identify()` or `user_properties` on the track call. ### Top referring domains ```plaintext | last 30d | count by event_properties.referrer | top 20 ``` Requires `referrer` passed in `event_properties` at track time. The JS SDK does not set this automatically — instrument it in your tracking code. ## Activation ### Signup-to-activation funnel ```plaintext funnel -> | last 30d ``` Two-step funnel. Returns user count at each step. Drop-off between steps = users who signed up but never activated. ### Multi-step funnel by platform ```plaintext funnel -> -> | last 30d | by _platform ``` Three-step funnel segmented by `web`, `android`, `ios`. Each platform gets independent step counts. ### Funnel with completion window ```plaintext funnel -> | last 30d | window 7d ``` Only counts users who activated within 7 days of signup. Default window is 30 days. ### Time to activate No built-in time-to-activate metric. Approximate it by querying user timelines: ```plaintext user "alice@acme.org" | last 90d | list ``` Compare timestamps of the signup and activation events in the result. For aggregate time-to-activate across users, use the funnel with progressively tighter windows (`window 1d`, `window 3d`, `window 7d`) and compare conversion rates. ## Retention ### Weekly cohort retention ```plaintext retention | last 90d ``` Groups users into weekly cohorts by their first signup. Shows return rates for weeks 1 through 12. Default granularity: `week`. ### Retention with custom return event ```plaintext retention returning | last 90d ``` Same cohort grouping, but only counts a return when the user performs the specified event. Without `returning`, any event counts. ### Monthly retention ```plaintext retention | last 6m | by month ``` Monthly cohorts. 6 months of data, up to 6 return periods each. ## Revenue ### Weekly revenue ```plaintext | last 12w | sum event_properties.amount by week ``` Total revenue per week. Requires `amount` as a numeric value in `event_properties` on purchase events. ### Average order value ```plaintext | last 30d | avg event_properties.amount ``` Mean purchase amount. For a distribution-resistant measure: ```plaintext | last 30d | median event_properties.amount ``` ### Conversion rate ```plaintext formula count() / count() | last 30d ``` Single decimal output (e.g. `0.12` = 12% conversion). For a weekly trend: ```plaintext formula count() / count() | last 12w | by week ``` Note `formula` queries are event-level metrics. They do not use stitched identity. For unique-user conversion, use a funnel instead. ## B2B segmentation ### Activity by company ```plaintext * | where user.email_domain = "acme.org" | last 30d | count by event_type ``` All events from users at a specific domain, broken down by type. Replace `"acme.org"` with the target company domain. ### Top companies by usage ```plaintext * | last 12w | count by user.email_domain | sort count desc | top 20 ``` Highest-volume companies by event count. Proxy for engagement and expansion potential. ### Weekly active users per company ```plaintext * | where user.email_domain = "acme.org" | last 12w | unique distinct_id by week ``` Unique users (stitched identity) per week at a specific company. Track seat expansion or contraction. ### Enterprise plan tracking ```plaintext | where user.plan = "enterprise" | last 12w | count by week ``` Weekly usage volume from enterprise-plan users. Requires `plan` set via `identify()`. ### Per-user activity within a company ```plaintext | where user.email_domain = "acme.org" | last 12w | count by week, user.email ``` Identify champions (high usage) and inactive seats (zero rows) within an account. ## B2C segmentation ### Users by plan ```plaintext users | count by user.plan | top 10 ``` Distribution of users across plans. Requires `plan` set via `identify()`. ### Paid vs free behavior Compare usage patterns between plan tiers: ```plaintext | where user.plan = "pro" | last 30d | count by day ``` ```plaintext | where user.plan = "free" | last 30d | count by day ``` Run both queries and compare the trends. Useful for understanding feature adoption across tiers. ### Unique users by plan over time ```plaintext * | where user.plan = "pro" | last 12w | unique distinct_id by week ``` Weekly active users on a specific plan. Track growth by tier. ## User investigation ### Single user timeline ```plaintext user "alice@acme.org" | last 90d | list ``` All events for this user in reverse chronological order. Matches on stitched `distinct_id` — anonymous events from before identification are included. ### User event breakdown ```plaintext user "alice@acme.org" | last 30d | count by event_type ``` What event types did this user trigger, and how many times? ### User directory ```plaintext users | where email_domain = "acme.org" | list ``` List all user profiles from a specific domain. Returns `user_id`, `email`, `email_domain`, `first_seen`, `last_seen`, and custom properties. Prerequisites for profile-based queries Queries using `user.*` fields (like `user.plan`, `user.email_domain`, `user.acquisition_channel`) require the `user_profiles` table to be populated. This happens via: * `POST /identify` calls with `user_properties` * `user_properties` sent on `POST /track` events If profile-based queries return empty results, verify that identify calls are being made. See [Identity](/identity/overview) for setup. # Identify API > POST /identify endpoint for device-user binding and profile management. Bind a `device_id` to a `user_id` and update user profile properties. No event is emitted. ## Endpoint ```plaintext POST /identify ``` ## Authentication `X-API-Key` header with a `pk_`, `sk_`, or `aat_` key (requires `track` scope). ## Request body | Field | Type | Required | Description | | ------------------- | ------ | -------- | -------------------------------------------------------- | | `user_id` | string | Yes | The identified user. | | `device_id` | string | No | Device to bind. Recommended for identity stitching. | | `user_properties` | object | No | Flat key-value map. Behaves like `$set` for convenience. | | `user_property_ops` | object | No | Granular property operations (see below). | ### user\_property\_ops | Operator | Type | Behavior | | ----------- | ------------------- | ----------------------------------------------------------------------------- | | `$set` | `map[string]any` | Overwrite properties. | | `$set_once` | `map[string]any` | Set only if the key does not already exist on the profile. | | `$add` | `map[string]number` | Numeric increment. Creates the key with the given value if it does not exist. | | `$unset` | `string[]` | Remove properties by key name. | Operations are applied in order: `user_properties` (as `$set`) -> `$set` -> `$set_once` -> `$add` -> `$unset`. ## Response ```json {"ok": true} ``` ## Behavior * If `device_id` is present: upserts `device_user_map` (`device_id` -> `user_id`). * Always: loads existing profile, applies ops in order, upserts to `user_profiles`. * `email_domain` is auto-extracted from the `email` property (e.g. `alice@acme.org` -> `acme.org`). * `first_seen` is set on the first identify call for a user. `last_seen` is updated on every call. * No event is emitted. This is purely an identity/profile operation. ## Recommended profile fields ### B2B | Field | Purpose | | -------------- | --------------------------------------- | | `email` | User identity, domain extraction | | `plan` | Segment by pricing tier | | `company_id` | Group users by account | | `company` | Human-readable company name | | `account_tier` | Enterprise / startup / SMB segmentation | ### B2C | Field | Purpose | | --------------------- | ------------------------------------ | | `email` | User identity, domain extraction | | `acquisition_channel` | Attribution (ads, organic, referral) | | `persona` | User archetype for segmentation | | `country` | Geographic segmentation | ## Examples ### Basic identify with device binding ```bash curl -X POST https://api.wirelog.ai/identify \ -H "X-API-Key: pk_YOUR_PUBLIC_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "alice@acme.org", "device_id": "dev_abc123", "user_properties": { "email": "alice@acme.org", "plan": "pro" } }' ``` ### Full property operations ```bash curl -X POST https://api.wirelog.ai/identify \ -H "X-API-Key: pk_YOUR_PUBLIC_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "alice@acme.org", "device_id": "dev_abc123", "user_property_ops": { "$set": {"plan": "enterprise", "company": "Acme Corp"}, "$set_once": {"signup_source": "product_hunt"}, "$add": {"login_count": 1}, "$unset": ["legacy_flag", "old_plan"] } }' ``` ### Profile update only (no device binding) ```bash curl -X POST https://api.wirelog.ai/identify \ -H "X-API-Key: sk_YOUR_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "alice@acme.org", "user_property_ops": { "$set": {"account_tier": "enterprise"}, "$add": {"api_calls": 42} } }' ``` Omitting `device_id` skips the device mapping upsert. The profile is still updated. ## Querying profile data After identify, profile fields are available in queries via `user.KEY`: ```text * | where user.plan = "enterprise" | last 30d | count by event_type * | where user.email_domain = "acme.org" | last 7d | unique distinct_id users | where email_domain = "acme.org" | list ``` ## Next steps * [Identity overview](/identity/overview) — how `distinct_id` stitching works * [HTTP tracking API](/tracking/http-api) — `POST /track` endpoint reference # Identity > Device-to-user identity stitching with distinct_id. Users start anonymous. They browse with a `device_id` but no `user_id`. When they log in or sign up, the `user_id` becomes known. WireLog stitches these into one identity automatically. ## Stitched identity: `distinct_id` ```plaintext distinct_id = coalesce(user_id, mapped_user_id, device_id) ``` Resolution order: 1. `user_id` — if the event has one, use it directly. 2. `mapped_user_id` — if the `device_id` has been bound to a user via `/identify`, use that mapping. 3. `device_id` — fallback for fully anonymous events. Use `unique distinct_id` in queries for unique user counts. This is the only field that correctly deduplicates across anonymous and identified sessions. ## device\_user\_map Maps `device_id` to `user_id`. Created by `POST /identify` calls. | Column | Type | Notes | | ------------ | ---------- | --------------------------- | | `project_id` | UUID | Scoped per project | | `device_id` | String | Anonymous device identifier | | `user_id` | String | Identified user identifier | | `updated_at` | DateTime64 | Latest mapping wins | Storage engine: ClickHouse `ReplacingMergeTree(updated_at)`, ordered by `(project_id, device_id)`. If the same device is identified to a different user, the latest mapping replaces the old one. ## user\_profiles Latest user profile state per user. Updated by `/identify` calls. | Column | Type | Notes | | ---------------------- | -------------------- | --------------------------------------------- | | `email` | String | Extracted from user properties | | `email_domain` | String | Auto-extracted from `email` (e.g. `acme.org`) | | `first_seen` | DateTime64 | Set on first `/identify` call | | `last_seen` | DateTime64 | Updated on every `/identify` call | | `user_properties` | Map(String, String) | Custom string properties | | `user_properties_num` | Map(String, Float64) | Custom numeric properties | | `user_properties_bool` | Map(String, UInt8) | Custom boolean properties | Storage engine: ClickHouse `ReplacingMergeTree(updated_at)`, ordered by `(project_id, user_id)`. Profile fields are queryable via `user.KEY`: ```text * | where user.email_domain = "acme.org" | last 30d | count by event_type * | where user.plan = "enterprise" | last 12w | count by week users | where email_domain = "acme.org" | list ``` ## Pre-identify attribution Anonymous events are attributed to the identified user once the device mapping exists. No backfill job runs. The query compiler resolves `distinct_id` at query time by joining `device_user_map`. This means: * Events tracked before `/identify` are retroactively attributed. * No data rewriting. No async backfill. No eventual consistency lag. * The mapping is read at query time, so it is always current. ## How to use 1. Send `device_id` on every event. Generate it client-side (UUID or similar) and persist it (localStorage, keychain, etc.). 2. Call `POST /identify` when the user is known — login, signup, or account link. 3. Use `unique distinct_id` in queries for unique user counts. ## Example flow ```text 1. User visits site (anonymous) -> track page_view, device_id="dev_abc" 2. User browses more pages -> track page_view, device_id="dev_abc" 3. User signs up -> POST /identify { user_id: "alice@acme.org", device_id: "dev_abc" } 4. All past events with device_id="dev_abc" now resolve to distinct_id="alice@acme.org" 5. Query: signup | last 30d | unique distinct_id -> Counts alice@acme.org once, including her anonymous session ``` ## Next steps * [Identify API](/identity/identify-api) — endpoint reference, property ops, curl examples * [Query language](/query-language/overview) — using `distinct_id`, `user.KEY`, and identity-aware queries # Introduction > Headless analytics for agents and LLMs. Events in, Markdown out. Headless analytics for AI agents and LLMs. Ingest events over HTTP, query with a pipe DSL, get Markdown tables back. No dashboards. Your agent is the dashboard. ## Track an event ```bash $ curl -X POST https://api.wirelog.ai/track \ -H "X-API-Key: pk_YOUR_PUBLIC_KEY" \ -H "Content-Type: application/json" \ -d '{ "event_type": "signup", "user_id": "alice@acme.org", "event_properties": {"plan": "pro", "source": "github"} }' ``` ```json {"accepted": 1} ``` ## Query it back ```bash $ curl -X POST https://api.wirelog.ai/query \ -H "X-API-Key: sk_YOUR_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"q": "signup | last 7d | count by day", "format": "llm"}' ``` ```markdown ## signup | last 7d | count by day | day | count | |------------|-------| | 2026-02-16 | 12 | | 2026-02-17 | 34 | | 2026-02-18 | 29 | | 2026-02-19 | 41 | | 2026-02-20 | 38 | | 2026-02-21 | 27 | | 2026-02-22 | 19 | ``` Output format is `llm` (Markdown) by default. Also supports `json` and `csv`. ## The pipe DSL Queries are source-first, pipe-composed. A source (event name, `*`, `funnel`, `retention`, `sessions`, `users`, or `user`) followed by stages separated by `|`. ```text # Count signups in the last 30 days signup | last 30d | count # Daily active users by platform page_view | last 7d | unique distinct_id by day, _platform # Signup-to-purchase funnel, split by source funnel signup -> activation -> purchase | last 30d | by event_properties.source # Weekly retention from signup retention signup | last 90d # All events for a specific user user "alice@acme.org" | last 30d | list # User directory filtered by domain users | where email_domain = "acme.org" | list ``` Stages: `where`, `last/from-to/today/this`, `count`, `unique`, `sum`, `avg`, `list`, `by`, `sort`, `limit`, `top`, `depth`. Full reference in [Query Language](/query-language/overview). Quickstart Track your first event in 5 minutes. [Start here](/quickstart) Query Language Pipe DSL reference — sources, stages, fields, operators. [Learn queries](/query-language/overview) For Agents MCP server, Claude Code skills, agent patterns. [Agent setup](/agents/overview) Pricing 10M events free. $5/million after. [See pricing](/reference/pricing) # Query Examples > 15+ annotated real-world queries organized by category. Replace placeholders with your event names Event names like ``, `` are placeholders. Discover your project’s actual event names first: ```plaintext * | last 30d | count by event_type | top 20 ``` ## Discovery Find what events exist and how frequently they fire. ### Top event types ```plaintext * | last 30d | count by event_type | top 20 ``` Returns the 20 most common event types by count over the last 30 days. Run this first in any new project. ### Full event inventory ```plaintext * | last 90d | count by event_type | sort event_type asc | limit 10000 ``` Alphabetical list of all event types with counts. Use `limit 10000` to avoid truncation on projects with many event types. ### Events today ```plaintext * | today | count by event_type | top 10 ``` Quick check of what is firing right now. ## Counts and trends ### Daily event count ```plaintext | last 7d | count by day ``` One row per day with the count of the specified event. Default sort is by time ascending. ### Weekly unique users ```plaintext | last 12w | unique distinct_id by week ``` Unique users (stitched identity) who triggered the event, grouped by week. ### Platform breakdown ```plaintext * | last 30d | count by _platform ``` Total events split by `web`, `android`, `ios`. Add `| top 10` if there are many values. ### Filtered count with multiple conditions ```plaintext | where _platform = "web" AND _browser = "Chrome" | last 30d | count by day ``` AND logic within a single `where`. Multiple `| where` clauses are also AND-ed. ## Funnels ### Basic conversion funnel ```plaintext funnel -> -> | last 30d ``` Returns step number and user count for each step. Step 1 = entered funnel, step 2 = completed second event, etc. Measures drop-off between steps. ### Funnel with platform breakout ```plaintext funnel -> -> | last 30d | by _platform ``` Same funnel, segmented by platform. Each platform gets its own set of step counts. ### Funnel with completion window ```plaintext funnel -> -> | last 90d | window 7d ``` Only counts users who completed the funnel within 7 days. Default window is 30 days. ## Retention ### Weekly cohort retention ```plaintext retention | last 90d ``` Groups users into weekly cohorts by their first ``. Shows how many return in weeks 1 through 12. Default granularity: `week`. ### Retention with specific return event ```plaintext retention returning | last 90d ``` Same cohort grouping, but only counts returns where the user did the specified event (not just any event). ### Monthly retention ```plaintext retention | last 6m | by month ``` Monthly cohorts instead of weekly. Returns 6 monthly cohorts with up to 6 periods each. ## User analysis ### Single user timeline ```plaintext user "alice@acme.org" | last 90d | list ``` All events for this user in reverse chronological order. Matches on stitched `distinct_id`, so anonymous events from before identification are included. ### User event breakdown ```plaintext user "alice@acme.org" | last 30d | count by event_type ``` What event types did this user trigger, and how many times? ### User directory listing ```plaintext users | where email_domain = "acme.org" | list ``` List all user profiles from the `acme.org` domain. Returns `user_id`, `email`, `email_domain`, `first_seen`, `last_seen`, and custom properties. Note `users` queries the `user_profiles` table. Profiles are populated by `identify()` calls. If a user has never been identified, they will not appear here. ### User count by plan ```plaintext users | count by user.plan | top 10 ``` How many users on each plan? Requires `plan` to be set via `identify()`. ## B2B segmentation ### Company activity ```plaintext * | where user.email_domain = "acme.org" | last 30d | count by event_type ``` All events from users at `acme.org`, broken down by type. Requires email to be set via `identify()`. ### Top companies by active users ```plaintext * | last 12w | count by user.email_domain | sort count desc | top 20 ``` Which companies have the most event volume? Proxy for engagement. ### Per-user activity within a company ```plaintext | where user.email_domain = "acme.org" | last 12w | count by week, user.email ``` Weekly usage per user at a specific company. Useful for identifying champions and inactive seats. ### Enterprise plan users over time ```plaintext * | where user.plan = "enterprise" | last 12w | unique distinct_id by week ``` Weekly unique enterprise-plan users. Track growth or contraction of the enterprise segment. ## Revenue and metrics ### Weekly revenue ```plaintext | last 12w | sum event_properties.amount by week ``` Total revenue per week. Requires `amount` in `event_properties` on purchase events. ### Conversion rate via formula ```plaintext formula count() / count() | last 30d ``` Single number: what fraction of signups convert to purchase? Output is a decimal (e.g. `0.12` = 12%). ### Weekly conversion rate trend ```plaintext formula count() / count() | last 12w | by week ``` Same ratio as a weekly time series. Track whether conversion is improving. ### Average order value ```plaintext | last 30d | avg event_properties.amount ``` Mean purchase amount. Use `median` for a distribution-resistant measure: ```plaintext | last 30d | median event_properties.amount ``` ## Sessions and paths ### Daily session metrics ```plaintext sessions | last 7d ``` Default output: session count, average duration (seconds), average events per session, grouped by day. ### Weekly session trends ```plaintext sessions | last 12w | count by week ``` Session count per week. Track engagement trends. ### User paths from an event ```plaintext paths from | last 30d ``` Most common event sequences after signup. Returns arrays of event types with user counts. Default depth: 5 steps, default limit: 20 paths. ### Paths leading to conversion ```plaintext paths to | last 30d | depth 8 ``` What do users do before purchasing? 8-step paths leading up to the purchase event. ### Paths with more results ```plaintext paths from | last 30d | depth 6 | limit 50 ``` Increase depth and result count for broader exploration. # Fields > Complete field reference -- core fields, system fields, property access, profile fields, and identity. Fields are used in `where` clauses, aggregations, `group by`, and `sort`. The available fields depend on the query source. ## Core fields Present on every event. Usable in all event-based queries. | Field | Type | Description | | ------------- | -------- | ------------------------------------------------------------- | | `event_type` | string | Event name (e.g. `signup`, `page_view`) | | `user_id` | string | User identifier set by your code | | `device_id` | string | Anonymous device identifier (auto-generated by JS SDK) | | `session_id` | string | Session identifier (auto-generated by JS SDK, 30-min timeout) | | `distinct_id` | string | Stitched identity (see [Identity](#identity) below) | | `time` | datetime | Event timestamp (client-provided or server `now()`) | | `insert_id` | string | Deduplication key (auto-generated if not provided) | ## System fields Server-enriched from the User-Agent header and request metadata. Prefixed with `_`. | Field | Type | Values | Description | | ------------------ | ------ | --------------------------------------------------- | ---------------------------------------- | | `_browser` | string | `Chrome`, `Firefox`, `Safari`, … | Browser name | | `_browser_version` | string | `120.0`, `3.1`, … | Browser version | | `_os` | string | `Windows`, `Mac OS X`, `Linux`, `Android`, `iOS`, … | Operating system | | `_os_version` | string | `14.2`, `10`, … | OS version | | `_platform` | string | `web`, `android`, `ios` | Platform (detected from UA) | | `_device_type` | string | `desktop`, `mobile`, `bot` | Device classification | | `_ip` | string | | Client IP (may be anonymized per policy) | | `_library` | string | `wirelog-js/1.0`, … | SDK identifier | **Examples:** ```plaintext # Events by platform * | last 7d | count by _platform # Mobile-only events * | where _device_type = "mobile" | last 30d | count by event_type | top 10 # Filter by browser page_view | where _browser = "Chrome" | last 7d | count ``` ## Event properties Access nested properties sent in the `event_properties` object on track calls. **Syntax:** `event_properties.` ```plaintext | where event_properties.page = "/pricing" | sum event_properties.amount by day | where event_properties.button contains "signup" ``` Property keys must match `[a-zA-Z0-9_.\-]+` and be at most 256 characters. **Examples:** ```plaintext # Filter by a custom property purchase | where event_properties.plan = "pro" | last 30d | count # Sum a numeric property purchase | last 30d | sum event_properties.amount by week # P95 of a latency property api_call | last 7d | p95 event_properties.duration_ms ``` ## User properties (on event) Access user properties sent alongside the event in the `user_properties` object. **Syntax:** `user_properties.` ```plaintext | where user_properties.plan = "enterprise" | count by user_properties.role ``` These are the user properties attached at track time, not the latest profile state. For latest profile state, use `user.`. ## Profile fields Access the `user_profiles` table for the latest user state. Populated by `identify()` calls. **Syntax:** `user.` ### Built-in profile fields | Field | Type | Description | | ------------------- | -------- | --------------------------------------------- | | `user.email` | string | Email address (set via identify) | | `user.email_domain` | string | Domain extracted from email (e.g. `acme.org`) | | `user.first_seen` | datetime | Timestamp of first event | | `user.last_seen` | datetime | Timestamp of most recent event | ### Custom profile properties Any key set via `identify()` user properties: ```plaintext user.plan user.company user.role user.acquisition_channel ``` These resolve to `user_properties['']` on the `user_profiles` table. **Availability by source:** | Source | `user.*` fields | Notes | | ------------------------------------------ | --------------- | ------------------------------------------------------------ | | Event queries (`*`, ``) | Yes | Joins `user_profiles` via stitched identity | | `user ""` | Yes | Joins `user_profiles` for the specified user | | `users` | Yes | Queries `user_profiles` directly | | `funnel`, `retention`, `paths`, `sessions` | No | Not supported; use event queries with `where user.*` instead | | `formula` | No | Event-level metrics only | **Examples:** ```plaintext # Events from enterprise users * | where user.plan = "enterprise" | last 30d | count by event_type # All events from a specific company * | where user.email_domain = "acme.org" | last 12w | count by week # User directory: list enterprise users users | where user.plan = "enterprise" | list # Count users by plan users | count by user.plan | top 10 ``` Caution Profile fields require `identify()` calls. If `user.*` filters return empty results, verify your app is sending identify calls with the expected properties. ## Identity `distinct_id` is the stitched identity field: ```plaintext coalesce(user_id, mapped_user_id, device_id) ``` Resolution order: 1. **`user_id`** — explicitly set user identifier 2. **`mapped_user_id`** — looked up from `device_user_map` (set by `identify()` binding a `device_id` to a `user_id`) 3. **`device_id`** — anonymous device identifier (fallback) Use `unique distinct_id` when you want unique user counts. This correctly deduplicates anonymous and identified events from the same user. ```plaintext # Unique users per week signup | last 12w | unique distinct_id by week # Unique users by platform * | last 30d | unique distinct_id by _platform ``` **Pre-identify attribution:** Events tracked before an `identify()` call are attributed retroactively. Once a device-to-user mapping exists in `device_user_map`, queries using `distinct_id` will include the anonymous events from that device. ## Property type handling Properties are normalized on ingest into typed storage buckets: | Type | Storage | Query behavior | | ------------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | String | `event_properties` / `user_properties` (canonical maps) | Direct string comparison | | Number | `*_num` maps + canonical string map | Numeric aggregations (`sum`, `avg`, etc.) use numeric bucket first, fall back to `toFloat64OrNull` on string | | Boolean | `*_bool` maps + canonical string map | Stored as `1`/`0` in bool bucket | | Null | `*_null` arrays + excluded from other maps | Key recorded in null array | | Object/Array | JSON-stringified in canonical string map | Stored as string; queryable via string operators | Queries automatically check the correct typed bucket. You do not need to reference `_num` or `_bool` maps directly — the compiler handles this. For numeric comparisons (`>`, `<`, `>=`, `<=`) with numeric literal values, the compiler uses the numeric bucket automatically. Non-numeric comparison values fall back to string semantics. For `exists`/`not exists` on property fields, the compiler checks `mapContains` on the canonical map. # Query Language Overview > WireLog's source-first, pipe-composed DSL for analytics queries. WireLog queries use a pipe-composed DSL. A source selects the data; stages transform it. ```plaintext source | stage | stage | ... ``` Every query starts with a **source** (what data), then zero or more **stages** separated by `|` (how to filter, aggregate, sort). Discover event names first Event names are project-specific. `signup`, `page_view`, `purchase` are examples only. Always run a discovery query before writing event-specific queries: ```plaintext * | last 30d | count by event_type | top 20 ``` Full inventory: ```plaintext * | last 90d | count by event_type | sort event_type asc | limit 10000 ``` ## Sources | Source | Syntax | Description | | --------------- | ----------------------------- | ----------------------------------------- | | Event | `` | Filter by `event_type` | | All events | `*` | All event types (leading `\|` also works) | | Funnel | `funnel a -> b -> c` | Multi-step conversion analysis | | Retention | `retention ` | Cohort retention over time | | Paths | `paths from ` | User flow analysis | | Sessions | `sessions` | Session-level analytics | | User timeline | `user ""` | Single user’s events (quoted) | | Users directory | `users` | Query user profiles table | | Formula | `formula count(a) / count(b)` | Computed metrics across event types | See [Sources](/query-language/sources/) for full reference. ## Stages | Stage | Syntax | Description | | -------------------- | ----------------------------------------------------------------- | ---------------------------------- | | Filter | `\| where field = "value"` | Filter rows by condition | | Time range | `\| last 7d`, `\| from ... to ...`, `\| today`, `\| this month` | Restrict time window | | Count | `\| count`, `\| count by day` | Count events, optionally grouped | | Unique | `\| unique distinct_id` | Count unique values | | Sum / Avg | `\| sum event_properties.amount` | Numeric aggregations | | Min / Max | `\| min field`, `\| max field` | Extremes | | Median / Percentiles | `\| median field`, `\| p90 field`, `\| p95 field`, `\| p99 field` | Distribution | | Group by | `\| count by day, field` | Group aggregation by time or field | | List | `\| list` | Return raw event rows | | Sort | `\| sort field desc` | Order results | | Limit / Top | `\| limit 100`, `\| top 20` | Cap result count | See [Stages](/query-language/stages/) for full reference. ## Request format Send queries via `POST /query` with a secret key (`sk_`) or access token (`aat_` with `query` scope): ```json { "q": "signup | last 7d | count by day", "format": "llm", "limit": 100, "offset": 0 } ``` | Field | Required | Default | Description | | -------- | -------- | ------- | ---------------------------------------------- | | `q` | Yes | | Query DSL string | | `format` | No | `llm` | Output format: `llm` (Markdown), `json`, `csv` | | `limit` | No | `100` | Max rows returned (max 10,000) | | `offset` | No | `0` | Pagination offset | Auth header: `X-API-Key: sk_...` or `X-API-Key: aat_...` ## Quick examples ```plaintext # Daily signups this week signup | last 7d | count by day # Conversion funnel funnel signup -> activate -> purchase | last 30d # Weekly retention cohorts retention signup | last 90d # All events from a specific company * | where user.email_domain = "acme.org" | last 30d | count by event_type # Single user timeline user "alice@acme.org" | last 90d | list # Conversion rate as a ratio formula count(purchase) / count(signup) | last 30d ``` ## Output formats **`llm`** (default) — Markdown table. Designed for LLM consumption. Includes column headers and aligned rows. **`json`** — Array of objects. Each row is a JSON object with column names as keys. **`csv`** — Comma-separated values with header row. ## Next * [Sources](/query-language/sources/) — event, funnel, retention, paths, sessions, user, users, formula * [Stages](/query-language/stages/) — where, time ranges, aggregations, sort, limit * [Fields](/query-language/fields/) — core, system, properties, profile fields, identity * [Examples](/query-language/examples/) — 15+ annotated real-world queries # Sources > Full reference for event, funnel, retention, paths, sessions, user, users, and formula sources. The source is the first token in every query. It determines what data the pipeline operates on. ## Event source Filter events by `event_type`. Use `*` for all events. **Syntax:** ```plaintext * ``` A leading `|` (with no source) is equivalent to `*`. **Description:** Selects rows from the `events` table matching the given event type. `*` selects all event types. This is the most common source — most queries start here. **Examples:** ```plaintext # Count page views in the last 7 days page_view | last 7d | count # Daily count of all events, broken down by type * | last 30d | count by day, event_type # Filter to web platform, list raw events signup | where _platform = "web" | last 7d | list ``` ## Funnel Multi-step conversion analysis. Measures how many users complete each step in sequence. **Syntax:** ```plaintext funnel -> -> [-> ...] ``` Requires at least 2 steps. Supports `| by ` for breakout by a dimension. Supports `| window ` to set the completion window (default: 30 days). **Description:** Computes a funnel using ClickHouse `windowFunnel`. For each user (`distinct_id`), it tracks the furthest step reached within the window. Returns step number and user count per step. Uses stitched identity — anonymous events are attributed if an identify mapping exists. **Examples:** ```plaintext # Basic 3-step funnel funnel signup -> activate -> purchase | last 30d # Funnel with platform breakout funnel signup -> activate -> purchase | last 30d | by _platform # Funnel with 7-day completion window funnel signup -> first_project -> first_query | last 90d | window 7d ``` Note Funnel steps must be event type names. The `by` stage is a standalone `| by field` (not `| count by`). Use it to break funnel results by a dimension like `_platform` or `_browser`. ## Retention Cohort retention analysis. Tracks what percentage of users who did a start event come back over subsequent time periods. **Syntax:** ```plaintext retention retention returning retention returning any ``` **Description:** Groups users into cohorts by the period they first did ``. Tracks how many return in subsequent periods. Default granularity is `week` (12 periods). Default time range is 12 weeks. `returning any` matches any event as the return action. If `returning` is omitted, any event counts. Supports `| by ` to change the cohort period (`day`, `week`, `month`). Supports `| by ` for breakdown. **Examples:** ```plaintext # Weekly retention for signups over 90 days retention signup | last 90d # Retention with a specific return event retention signup returning purchase | last 90d # Monthly retention cohorts retention signup | last 6m | by month # Daily retention retention signup returning login | last 30d | by day ``` ## Paths User flow analysis. Shows common event sequences starting from or leading to a given event. **Syntax:** ```plaintext paths from paths to ``` **Description:** `paths from` collects event sequences starting at `` for each user, within a 1-hour window after the start event. `paths to` collects sequences leading up to `` within 1 hour before it. Results are grouped by path (array of event types) with user counts. Default depth is 5 steps. Default limit is 20 paths. Supports `| depth ` to control path length. Supports `| limit ` to control number of paths returned. **Examples:** ```plaintext # What do users do after signing up? paths from signup | last 30d # What leads users to purchase? paths to purchase | last 30d # Longer paths with more results paths from signup | last 30d | depth 8 | limit 50 ``` ## Sessions Session-level analytics. Groups events by `session_id` and computes per-session metrics. **Syntax:** ```plaintext sessions ``` **Description:** Aggregates events into sessions (grouped by `session_id` + `distinct_id`). Default output: session count, average duration (seconds), and average events per session, grouped by day. Requires events to have `session_id` set (the JS SDK sets this automatically with a 30-minute inactivity timeout). Supports time granularity grouping (`| count by day`, `| count by week`). **Examples:** ```plaintext # Daily session metrics for the last 7 days sessions | last 7d # Weekly session metrics sessions | last 12w | count by week # Sessions for a specific time range sessions | from 2026-01-01 to 2026-02-01 ``` ## User timeline All events for a single user, ordered by time. **Syntax:** ```plaintext user "" ``` The user ID must be quoted. Matches against `distinct_id` (stitched identity: `coalesce(user_id, mapped_user_id, device_id)`). **Description:** Returns all events for the specified user. Default time range is 30 days. Default output is a chronological event list (most recent first). Supports aggregation stages if you want counts instead of raw events. **Examples:** ```plaintext # Last 90 days of activity for a user user "alice@acme.org" | last 90d | list # Count events by type for a user user "alice@acme.org" | last 30d | count by event_type # Recent events (default 30d, default list) user "u_abc123" ``` Tip Use the email address or user ID you set via `identify()`. The query matches against the stitched `distinct_id`, so it finds events from before and after identification. ## Users directory Query the `user_profiles` table. Browse, filter, and count users. **Syntax:** ```plaintext users ``` **Description:** Queries the `user_profiles` table (populated by identify calls and ingestion-side profile upserts). Defaults to users active in the last 30 days (by `last_seen`). Supports `| where` for filtering, `| list` for raw profiles, `| count` for totals, and `| count by ` for breakdowns. Available fields: `user_id`, `email`, `email_domain`, `first_seen`, `last_seen`, `user.KEY` (custom profile properties). **Examples:** ```plaintext # List users from a specific domain users | where email_domain = "acme.org" | list # Count users by email domain users | count by email_domain | top 20 # Count users on the enterprise plan users | where user.plan = "enterprise" | count # Users active this month users | this month | list ``` ## Formula Computed metrics across event types. Combine count, unique, sum, or avg aggregates with arithmetic operators. **Syntax:** ```plaintext formula ([, field]) ([, field]) [ ...] ``` Supported functions: `count`, `unique`, `sum`, `avg`. Supported operators: `+`, `-`, `*`, `/`. **Description:** Each metric reference queries a specific event type. The formula combines them with arithmetic. Useful for conversion rates, ratios, and computed KPIs. For `sum` and `avg`, a second argument specifies the field. For `unique`, an optional second argument specifies the field (defaults to `user_id`). Supports `| by ` for time-series output. **Examples:** ```plaintext # Conversion rate: purchases per signup formula count(purchase) / count(signup) | last 30d # Weekly conversion rate trend formula count(purchase) / count(signup) | last 12w | by week # Average revenue per user formula sum(purchase, event_properties.amount) / unique(purchase) | last 30d ``` Note Formula queries are event-level metrics. They do not use stitched identity for `count`. For unique user counts with identity stitching, prefer event queries with `| unique distinct_id`. # Stages > Full reference for where, time ranges, aggregations, list, sort, limit, and other pipeline stages. Stages are pipe-separated transformations applied after the source. They filter, aggregate, sort, and limit results. ```plaintext source | stage | stage | ... ``` Stages are evaluated left-to-right. Order matters: `| where ... | last 7d | count by day` filters first, then restricts time, then aggregates. ## where Filter rows by field conditions. **Syntax:** ```plaintext | where | where AND | where OR ``` **Operators:** | Operator | Description | Example | | -------------- | ------------------------------ | --------------------------------------------------- | | `=` | Equals | `\| where _platform = "web"` | | `!=` | Not equals | `\| where _browser != "Safari"` | | `>` | Greater than | `\| where event_properties.amount > 100` | | `<` | Less than | `\| where event_properties.count < 5` | | `>=` | Greater than or equal | `\| where event_properties.score >= 80` | | `<=` | Less than or equal | `\| where event_properties.duration <= 30` | | `contains` | Substring match | `\| where event_properties.page contains "/blog"` | | `not contains` | No substring match | `\| where event_properties.url not contains "test"` | | `~` | Regex match | `\| where event_properties.page ~ "^/docs/.*"` | | `!~` | Regex not match | `\| where _browser !~ "bot"` | | `in` | Value in list | `\| where _platform in ("web", "android")` | | `not in` | Value not in list | `\| where _browser not in ("Safari", "IE")` | | `exists` | Field is present and non-empty | `\| where event_properties.referrer exists` | | `not exists` | Field is absent or empty | `\| where event_properties.error not exists` | Multiple conditions in a single `where` are combined with `AND` or `OR`. Evaluated left-to-right, no parentheses grouping. Multiple `| where` clauses are always AND-ed together. **Examples:** ```plaintext # Simple equality signup | where _platform = "web" | last 7d | count # Numeric comparison (uses typed numeric bucket automatically) purchase | where event_properties.amount > 50 | last 30d | count # OR condition * | where _platform = "web" OR _platform = "android" | last 7d | count # Combined AND + OR (left-to-right evaluation) * | where _platform = "web" AND _browser = "Chrome" OR _browser = "Firefox" | last 7d | count # Substring match page_view | where event_properties.page contains "/pricing" | last 30d | count # Exists check on property maps * | where event_properties.error exists | last 7d | list # Profile-based filter * | where user.plan = "enterprise" | last 30d | count by event_type ``` Note Numeric comparisons (`>`, `<`, `>=`, `<=`) with numeric literal values automatically use the typed numeric property bucket for correctness. Non-numeric values fall back to string comparison. ## Time ranges Restrict the query to a time window. Without a time range, queries default to the last 7 days (event queries) or last 30 days (user queries). ### Relative duration ```plaintext | last ``` | Unit | Meaning | Example | | ---- | ------- | ------------- | | `m` | Minutes | `\| last 30m` | | `h` | Hours | `\| last 24h` | | `d` | Days | `\| last 7d` | | `w` | Weeks | `\| last 12w` | ### Absolute range ```plaintext | from to | from to ``` The `from` date is inclusive, `to` is exclusive. ### Shortcuts ```plaintext | today | yesterday | this week | this month | this quarter | this year ``` `this week` starts on Monday. `this month` starts on the 1st. `this quarter` and `this year` start at the beginning of the current quarter/year. **Examples:** ```plaintext # Last 24 hours * | last 24h | count by event_type | top 10 # Specific date range signup | from 2026-01-01 to 2026-02-01 | count by day # This month so far purchase | this month | sum event_properties.amount # Today only * | today | count by event_type ``` ## Aggregations Compute metrics over the selected data. If no aggregation stage is specified, event queries default to `| count`. ### count Count rows. ```plaintext | count | count by , , ... ``` ### unique Count distinct values of a field. ```plaintext | unique | unique by , , ... ``` If no field is specified, defaults to `distinct_id` (unique users) for event queries, `user_id` for users queries. ### sum Sum a numeric field. ```plaintext | sum | sum by , , ... ``` Field is required. Non-numeric values are ignored (NULL), not coerced to zero. ### avg Average of a numeric field. ```plaintext | avg | avg by , , ... ``` ### min / max Minimum or maximum of a numeric field. ```plaintext | min | max ``` ### median Median (50th percentile) of a numeric field. ```plaintext | median | median by ``` ### Percentiles: p90, p95, p99 ```plaintext | p90 | p95 | p99 ``` **Examples:** ```plaintext # Total events signup | last 30d | count # Unique users per week * | last 12w | unique distinct_id by week # Revenue by day purchase | last 30d | sum event_properties.amount by day # Average order value by platform purchase | last 30d | avg event_properties.amount by _platform # P95 response time api_request | last 7d | p95 event_properties.duration # Unique users by week and platform signup | last 12w | unique distinct_id by week, _platform ``` ## Group by The `by` keyword groups aggregation results. Used inline with aggregations (`| count by day`) or as a standalone stage (`| by _platform`) for funnel/retention breakouts. ### Time granularities | Granularity | Truncation | Alias | | ----------- | ------------------ | --------- | | `hour` | Start of hour | `hour` | | `day` | Start of day | `day` | | `week` | Monday of the week | `week` | | `month` | 1st of the month | `month` | | `quarter` | Start of quarter | `quarter` | ### Field grouping Group by any queryable field. Combine with time granularity using commas: ```plaintext | count by day, _platform | unique distinct_id by week, _browser | sum event_properties.amount by month, user.plan ``` ## list Return raw event rows instead of aggregated results. ```plaintext | list ``` Default columns returned: `insert_id`, `event_type`, `time`, `user_id`, `device_id`, `session_id`, `event_properties`, `user_properties`, `_ip`, `_browser`, `_browser_version`, `_os`, `_os_version`, `_device_type`, `_platform`. Results are ordered by `time DESC` (most recent first). Use `| limit N` to control how many rows. **Examples:** ```plaintext # Raw events for a specific type error | last 24h | list # Raw events from a user user "alice@acme.org" | last 7d | list # User profiles as a list users | where email_domain = "acme.org" | list ``` ## sort Order results by a field. ```plaintext | sort desc | sort asc ``` Default direction is `desc` if omitted. Can sort by aggregation output columns (`count`, `unique_count`, `total`, etc.) or by data fields. **Examples:** ```plaintext # Sort by count ascending * | last 30d | count by event_type | sort count asc # Sort by a field users | list | sort last_seen desc ``` ## limit / top Cap the number of result rows. ```plaintext | limit | top ``` `top N` is shorthand for `| sort count desc | limit N` — it sorts by the metric column descending and takes the first N rows. `limit N` only caps without reordering. Maximum value: 10,000. **Examples:** ```plaintext # Top 20 event types by count * | last 30d | count by event_type | top 20 # Limit raw event listing to 50 rows error | last 7d | list | limit 50 # Top 10 browsers by unique users * | last 30d | unique distinct_id by _browser | top 10 ``` ## Funnel/retention-specific stages ### window Set the funnel completion window. Only valid with `funnel` source. ```plaintext | window ``` Default: 30 days. Duration uses the same units as `last` (`m`, `h`, `d`, `w`). ### depth Set the maximum path length. Only valid with `paths` source. ```plaintext | depth ``` Default: 5 steps. ### by (standalone) Breakout dimension for funnel or retention results. Not an aggregation grouping. ```plaintext | by | by , ``` **Examples:** ```plaintext # Funnel with 7-day window, broken out by platform funnel signup -> activate -> purchase | last 30d | window 7d | by _platform # Paths with 8-step depth paths from signup | last 30d | depth 8 ``` # Quickstart > Track your first event and query it back in 5 minutes. ## 1. Sign up Go to [wirelog.ai](https://wirelog.ai) and sign in with GitHub. A default organization is created automatically. ## 2. Create a project From the dashboard, create a new project. You get two API keys: | Key | Prefix | Use | | ---------- | -------- | ---------------------------------------- | | Public key | `pk_...` | Client-safe. Track events only. | | Secret key | `sk_...` | Server-side only. Track + query + admin. | Copy both. The secret key is shown once. ## 3. Track your first event ```bash $ curl -X POST https://api.wirelog.ai/track \ -H "X-API-Key: pk_YOUR_PUBLIC_KEY" \ -H "Content-Type: application/json" \ -d '{ "event_type": "signup", "user_id": "alice@acme.org", "event_properties": {"plan": "pro", "source": "github"} }' ``` ```json {"accepted": 1} ``` `event_type` is the only required field. `user_id`, `device_id`, `session_id`, `event_properties`, and `user_properties` are optional. Batch multiple events with `{"events": [...]}`. ## 4. Query it back ```bash $ curl -X POST https://api.wirelog.ai/query \ -H "X-API-Key: sk_YOUR_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"q": "* | last 1d | count by event_type", "format": "llm"}' ``` ```markdown ## * | last 1d | count by event_type | event_type | count | |------------|-------| | signup | 1 | ``` The `format` field controls output: `llm` (Markdown, default), `json`, or `csv`. Queries require a secret key (`sk_`) or an access token with `query` scope. ## 5. Add the JS SDK (optional) For browser tracking, drop a single script tag: ```html ``` This auto-tracks `page_view` on load, manages `device_id` via localStorage, and handles `session_id` with a 30-minute inactivity timeout. Track custom events and identify users in JS: ```javascript // Track a custom event wl.track("button_click", { button: "upgrade", page: "/pricing" }); // Identify a logged-in user wl.identify("alice@acme.org", { plan: "pro", company: "Acme" }); ``` ## 6. Set up identity Bind a device to a known user so pre-login events get stitched to the same `distinct_id`: ```bash $ curl -X POST https://api.wirelog.ai/identify \ -H "X-API-Key: pk_YOUR_PUBLIC_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "alice@acme.org", "device_id": "dev_abc123", "user_properties": {"email": "alice@acme.org", "plan": "pro"}, "user_property_ops": { "$set": {"plan": "pro"}, "$set_once": {"signup_source": "github"}, "$add": {"login_count": 1} } }' ``` ```json {"ok": true} ``` `user_id` is required. `device_id` creates the device-to-user mapping. `user_property_ops` supports `$set`, `$set_once`, `$add`, and `$unset` for incremental profile updates. ## Next steps * [Query language](/query-language/overview) — pipe DSL reference with sources, stages, and operators * [Agent setup](/agents/overview) — MCP server, Claude Code skills, and agent query patterns * [SaaS metrics](/guides/saas-metrics) — funnel, retention, and segmentation recipes # API Reference > Complete HTTP API reference for WireLog. All requests and responses are JSON unless noted. Authentication via `X-API-Key` header or `key` query parameter (API keys) or `wl_session` cookie (session auth). ## Public Endpoints No authentication required. | Method | Path | Description | | ------ | ----------------- | --------------------------- | | `GET` | `/` | Landing page | | `GET` | `/login` | Login page | | `GET` | `/auth/github` | GitHub OAuth initiation | | `GET` | `/auth/github/cb` | GitHub OAuth callback | | `POST` | `/auth/logout` | Destroy session | | `GET` | `/public/*` | Static assets (JS SDK, CSS) | | `GET` | `/llms.txt` | LLM-readable site summary | *** ## API Key Endpoints ### POST /track Ingest events. Single or batch. **Auth:** `pk_`, `sk_`, or `aat_` with `track` scope. Single event: ```json { "event_type": "page_view", "user_id": "u123", "device_id": "d456", "session_id": "s789", "time": "2026-01-15T10:30:00Z", "event_properties": {"page": "/home"}, "user_properties": {"plan": "pro"}, "insert_id": "optional-dedup-id" } ``` Batch: ```json { "events": [ {"event_type": "click", "event_properties": {"button": "signup"}}, {"event_type": "page_view", "event_properties": {"page": "/pricing"}} ] } ``` **Response:** ```json {"accepted": 2} ``` Invalid events in a batch are silently skipped. *** ### POST /identify Bind a device to a user. Update user profile properties. **Auth:** `pk_`, `sk_`, or `aat_` with `track` scope. ```json { "user_id": "alice@acme.org", "device_id": "dev_123", "user_properties": {"email": "alice@acme.org", "plan": "pro"}, "user_property_ops": { "$set": {"plan": "pro"}, "$set_once": {"signup_source": "ads"}, "$add": {"login_count": 1}, "$unset": ["legacy_flag"] } } ``` **Response:** ```json {"ok": true} ``` *** ### POST /query Run a pipe DSL query. **Auth:** `sk_` or `aat_` with `query` scope. ```json { "q": "event_type = \"page_view\" | last 7d | count by _browser", "format": "llm", "limit": 100, "offset": 0 } ``` | Field | Required | Default | Notes | | -------- | -------- | ------- | ---------------------------------------- | | `q` | Yes | — | Pipe DSL query string | | `format` | No | `"llm"` | `"llm"` (Markdown), `"json"`, or `"csv"` | | `limit` | No | `100` | Max 10,000 | | `offset` | No | `0` | Pagination offset | **Response:** Markdown table, JSON array, or CSV depending on `format`. *** ## Session-Authenticated API All `/api/*` routes require the `wl_session` cookie (set by GitHub OAuth login). ### Organizations **GET /api/orgs** — List user’s orgs. **POST /api/orgs** — Create org. ```json {"name": "Acme Corp"} ``` **GET /api/orgs/{orgID}** — Get org details. **POST /api/orgs/{orgID}/rotate-admin-key** — Rotate the org admin key (`ak_`). ```json {"admin_key": "ak_..."} ``` *** ### Projects **GET /api/orgs/{orgID}/projects** — List org projects. **POST /api/orgs/{orgID}/projects** — Create project. ```json {"name": "My App"} ``` Returns project with `public_key` (`pk_`) and `secret_key` (`sk_`). **GET /api/projects/{projectID}** — Get project (includes keys). **DELETE /api/projects/{projectID}** — Delete project permanently. ```json { "project_name": "My App", "project_name_confirm": "My App" } ``` Both fields must match exactly. Deletes project and all associated data. **POST /api/projects/{projectID}/rotate-secret-key** — Rotate `sk_` key. Public key unchanged. *** ### Access Tokens **POST /api/projects/{projectID}/tokens** — Create scoped access token. ```json { "name": "ci-pipeline", "scopes": ["track", "query"], "expires_in": "720h" } ``` Returns the raw token once. It cannot be retrieved again. **GET /api/projects/{projectID}/tokens** — List tokens (prefix only). **DELETE /api/projects/{projectID}/tokens/{tokenID}** — Revoke token. *** ### Usage **GET /api/projects/{projectID}/usage** — Daily usage stats. Query params: `?from=YYYY-MM-DD&to=YYYY-MM-DD`. Defaults to current month. *** ## Admin API Auth: org admin key (`ak_`) via `X-API-Key` header. All routes under `/api/admin/*`. | Method | Path | Description | | -------- | --------------------------------------------------- | ------------------------------------------------------ | | `GET` | `/api/admin/projects` | List org projects | | `POST` | `/api/admin/projects` | Create project | | `GET` | `/api/admin/projects/{projectID}` | Get project (includes keys) | | `DELETE` | `/api/admin/projects/{projectID}` | Delete project (same confirmation body as session API) | | `POST` | `/api/admin/projects/{projectID}/rotate-secret-key` | Rotate `sk_` key | | `GET` | `/api/admin/projects/{projectID}/usage` | Daily usage stats | *** ## GDPR Auth: `sk_` or `aat_` with `admin` scope via `X-API-Key` header. **DELETE /api/gdpr/users/{userID}** — Delete all user data: events, identity mappings, and profile rows. Logged to audit trail. **GET /api/gdpr/users/{userID}/export** — Stream all user events as NDJSON. Logged to audit trail. *** ## API Key Types | Prefix | Type | Scopes | Client-safe? | | ------ | ------------ | ------------------------- | ----------------- | | `pk_` | Public key | `track` | Yes | | `sk_` | Secret key | `track`, `query`, `admin` | No | | `ak_` | Admin key | org admin | No | | `aat_` | Access token | custom per-token | Depends on scopes | * `pk_` keys are safe to expose in client-side code. They can only ingest events. * `sk_` keys have full project access. Never expose in browsers or public repos. * `ak_` keys have org-level admin access. Never expose in client code. * `aat_` tokens are hashed before storage. The raw token is shown once at creation. *** ## Error Format All errors return: ```json {"error": "message"} ``` | Code | Meaning | | ----- | ------------------------------------------------ | | `400` | Bad request | | `401` | Missing or invalid authentication | | `403` | Insufficient scope or limit reached | | `404` | Not found | | `413` | Payload too large | | `429` | Rate limited (includes `Retry-After: 60` header) | | `503` | Feature disabled | | `504` | Query timeout | # Pricing > 10M events free. $5 per million after. No per-seat. No tiers. ## The Price ```plaintext Free 10M events/month 90-day retention 3 projects Paid $5 per million events ``` No tiers. No per-seat. No “contact sales.” *** ## Free Tier Included for every org, no credit card required. * 10M events/month * 90-day data retention * 3 projects per org * All query features: funnels, retention, paths, sessions, formulas * Identity stitching * JS SDK * Full API access (`/track`, `/query`, `/identify`) *** ## Paid Usage-based. Monthly billing. No commitments. Cancel anytime. | | Free | Paid | | ------------------ | ------- | --------------------------- | | Events/month | 10M | 10M free + $5/million after | | Data retention | 90 days | 1 year | | Projects per org | 3 | Unlimited | | Query features | All | All | | Identity stitching | Yes | Yes | | API access | Full | Full | First 10M events/month are always free. You only pay for events beyond 10M. *** ## Cost Comparison Monthly cost by volume. Numbers speak for themselves. | Volume | WireLog | PostHog | Amplitude | Mixpanel | | ---------- | ---------- | ---------- | -------------- | --------------- | | 10M/month | **$0** | \~$500 | \~$49-200 | \~$2,500 | | 50M/month | **$200** | \~$1,500 | \~$500-1,500 | \~$5,000+ | | 100M/month | **$450** | \~$2,500 | \~$1,500-3,000 | \~$5,000-10,000 | | 1B/month | **$4,950** | \~$15,000+ | Contact sales | Contact sales | *** ## What We Don’t Do * No per-seat pricing. * No MTU math. * No sales calls. * No surprise bills. * No annual contracts. * No feature gating. *** ## Beta Billing is not yet live. Everything is free during beta. # HTTP API > Track events, identify users, and query analytics via HTTP. All endpoints accept `Content-Type: application/json`. All responses are JSON unless otherwise noted. ## Authentication Pass your API key via `X-API-Key` header or `key` query parameter. | Key type | Prefix | Allowed endpoints | | ------------ | ------ | ----------------------------------- | | Public key | `pk_` | `/track`, `/identify` | | Secret key | `sk_` | `/track`, `/identify`, `/query` | | Access token | `aat_` | Per-token scopes (`track`, `query`) | *** ## POST /track Track one or more events. ### Single event ```json { "event_type": "page_view", "user_id": "u_123", "device_id": "d_456", "session_id": "s_789", "time": "2026-01-15T10:30:00Z", "event_properties": { "page": "/pricing" }, "user_properties": { "plan": "pro" }, "insert_id": "dedup-key-123", "library": "wirelog-python/0.1" } ``` | Field | Type | Required | Notes | | ------------------ | ------ | -------- | -------------------------------------------- | | `event_type` | string | Yes | Non-empty | | `user_id` | string | No | User identifier | | `device_id` | string | No | Anonymous device identifier | | `session_id` | string | No | Session identifier | | `time` | string | No | RFC 3339. Defaults to server time | | `event_properties` | object | No | Arbitrary key-value pairs | | `user_properties` | object | No | Arbitrary key-value pairs | | `insert_id` | string | No | Deduplication key. Auto-generated if omitted | | `library` | string | No | SDK identifier string | ### Batch ```json { "events": [ { "event_type": "click", "event_properties": { "button": "signup" } }, { "event_type": "page_view", "event_properties": { "page": "/docs" } } ] } ``` Maximum 2000 events per batch. ### Response ```json { "accepted": 2 } ``` `accepted` is the count of events that passed validation. Invalid events are silently skipped. ### Validation * `event_type` required and non-empty * Property count within `max_properties_count` / `max_user_props_count` limits * Property keys within `max_property_key_len` * Property values within `max_property_value_len` * Request body within `max_request_bytes` (413 if exceeded) * Batch size within `max_batch_size` (400 if exceeded) ### Server-side enrichment Every event is enriched before storage: * **UA parsing**: `_browser`, `_browser_version`, `_os`, `_os_version` extracted from `User-Agent` * **Device type**: `_device_type` = `mobile`, `bot`, or `desktop` * **Platform**: `_platform` = `android`, `ios`, or `web` * **IP handling**: raw or anonymized per project policy * **Property normalization**: values split into typed buckets (string, numeric, boolean, null) for efficient querying * **Insert ID**: auto-generated (`evt_` + 16 random chars) if not provided * **Time**: parsed as RFC 3339. Falls back to server time if unparseable ### Deduplication The ClickHouse `events` table uses `ReplacingMergeTree` with `ORDER BY (project_id, event_type, time, insert_id)`. Events sharing the same order key are deduplicated at merge time. *** ## POST /identify Bind a `device_id` to a `user_id` and upsert user profile properties. No event is emitted. ```json { "user_id": "alice@acme.org", "device_id": "dev_abc", "user_properties": { "email": "alice@acme.org", "plan": "pro" }, "user_property_ops": { "$set": { "plan": "enterprise" }, "$set_once": { "signup_source": "ads" }, "$add": { "login_count": 1 }, "$unset": ["legacy_flag"] } } ``` | Field | Type | Required | Notes | | ------------------- | ------ | -------- | --------------------- | | `user_id` | string | Yes | User identifier | | `device_id` | string | No | Device to bind | | `user_properties` | object | No | Flat set (overwrites) | | `user_property_ops` | object | No | Granular operations | **Property operations:** | Operator | Effect | | ----------- | ----------------------------------- | | `$set` | Set properties (overwrite existing) | | `$set_once` | Set only if not already present | | `$add` | Increment numeric properties | | `$unset` | Remove properties (array of keys) | ### Response ```json { "ok": true } ``` ### Behavior * Inserts/updates a row in `device_user_map` linking `device_id` to `user_id` * Upserts `user_profiles` with the provided properties and operations * `user_properties` (flat set) and `user_property_ops` can be used together *** ## POST /query Run a pipe DSL query against stored events. **Auth**: `sk_` or `aat_` with `query` scope. ```json { "q": "page_view | last 7d | count by _browser", "format": "llm", "limit": 100, "offset": 0 } ``` | Field | Type | Required | Default | Notes | | -------- | ------- | -------- | ------- | ---------------------------------------- | | `q` | string | Yes | | Pipe DSL query string | | `format` | string | No | `"llm"` | `"llm"` (Markdown), `"json"`, or `"csv"` | | `limit` | integer | No | `100` | Max 10,000 | | `offset` | integer | No | `0` | For pagination | ### Response Depends on `format`: * **`llm`**: Markdown table (Content-Type: `text/plain`) * **`json`**: JSON array of objects (Content-Type: `application/json`) * **`csv`**: CSV string (Content-Type: `text/csv`) *** ## Errors All errors return JSON: ```json { "error": "message" } ``` | Status | Meaning | | ------ | ----------------------------------------------------------- | | 400 | Bad request (invalid JSON, missing fields, batch too large) | | 401 | Missing or invalid API key | | 403 | Insufficient scope or membership | | 404 | Not found | | 413 | Payload too large | | 429 | Rate limited. Includes `Retry-After: 60` header | | 503 | Feature disabled | | 504 | Query timed out | # JS SDK > Browser tracking with the WireLog JavaScript SDK. Lightweight browser SDK. No build step. Single script tag. Sends events via `sendBeacon` with XHR fallback. ## Installation ```html ``` ### Script attributes | Attribute | Required | Description | | -------------- | ----------- | ------------------------------------------------------- | | `data-key` | Yes | Public API key (`pk_...`) | | `data-host` | Recommended | API base URL. Falls back to inferring from script `src` | | `data-consent` | No | `"true"` to require `optIn()` before any tracking | | `data-auto` | No | `"false"` to disable automatic `page_view` on load | | `data-spa` | No | `"true"` to auto-track `page_view` on SPA navigations | *** ## Automatic behavior * Tracks `page_view` on page load (unless `data-auto="false"`) * SPA mode (`data-spa="true"`) intercepts `pushState`, `replaceState`, and `popstate` to fire `page_view` on navigation * Auto-captured `page_view` properties: `url`, `referrer`, `title` *** ## Batching and delivery * Events queue locally, flush on **10 events** or every **2 seconds** * Uses `navigator.sendBeacon` for reliable delivery on page close; falls back to XHR if `sendBeacon` returns `false` * Flushes on `visibilitychange` (hidden) and `pagehide` *** ## Identity management | Storage | Key | Persistence | | ------------ | ------------------------ | ------------------------------------------- | | `device_id` | `wl_did` in localStorage | Permanent until `reset()` or `optOut()` | | `session_id` | In-memory | Regenerated after 30 minutes of inactivity | | `user_id` | `wl_uid` in localStorage | Set via `identify()`, cleared via `reset()` | *** ## Queue * Capped at **500 events** * On overflow, oldest events are dropped (FIFO eviction) *** ## API reference Available as `window.wirelog` and `window.wl`. ### wl.track(eventType, eventProps?, userProps?) Track a custom event. ```js wl.track("button_click", { button: "signup" }); wl.track("purchase", { amount: 49.99, plan: "pro" }, { plan: "pro" }); ``` ### wl.identify(userId, userProps?, userPropOps?) Set the user ID, persist it in localStorage, and send a `POST /identify` call. ```js wl.identify("alice@acme.org", { plan: "pro" }); wl.identify("alice@acme.org", null, { $set: { plan: "enterprise" }, $set_once: { signup_source: "organic" }, $add: { login_count: 1 }, $unset: ["legacy_flag"] }); ``` The user ID is attached to all subsequent `track()` calls until `reset()`. ### wl.reset() Clear identity state. Use on logout. ```js wl.reset(); ``` Removes `user_id`, `device_id`, and `session_id`. Generates a new `device_id`. ### wl.optIn() Enable tracking and persist consent in localStorage (`wl_consent`). ```js wl.optIn(); ``` ### wl.optOut() Disable tracking. Clears the event queue and all stored identifiers. ```js wl.optOut(); ``` ### wl.flush() Manually flush the event queue. ```js wl.flush(); ``` *** ## Consent mode Set `data-consent="true"` on the script tag. No events are tracked or queued until `optIn()` is called. Consent state persists in localStorage across page loads. ```html ``` # Node.js Client > Zero-dependency TypeScript/Node.js analytics client. Zero runtime dependencies. Uses native `fetch` (Node 18+). Full TypeScript types included. ESM and CJS exports. ## Install ```bash npm install wirelog ``` ## Quick start ```ts import { WireLog } from "wirelog"; const wl = new WireLog({ apiKey: "sk_your_secret_key", host: "https://yourhost.com" }); await wl.track({ event_type: "page_view", user_id: "alice" }); const result = await wl.query("page_view | last 7d | count by user_id"); console.log(result); ``` *** ## Constructor ```ts new WireLog(config?: WireLogConfig) ``` ```ts interface WireLogConfig { apiKey?: string; // Falls back to WIRELOG_API_KEY env var host?: string; // Falls back to WIRELOG_HOST env var or https://api.wirelog.ai } ``` *** ## Types ```ts interface TrackEvent { event_type: string; // Required user_id?: string; device_id?: string; session_id?: string; time?: string; // RFC 3339. Auto-generated if omitted event_properties?: Record; user_properties?: Record; insert_id?: string; // Auto-generated (randomUUID) if omitted } interface TrackResult { accepted: number; } interface IdentifyParams { user_id: string; // Required device_id?: string; user_properties?: Record; user_property_ops?: { $set?: Record; $set_once?: Record; $add?: Record; $unset?: string[]; }; } interface IdentifyResult { ok: boolean; } interface QueryOptions { format?: "llm" | "json" | "csv"; limit?: number; // Default 100, max 10,000 offset?: number; } ``` *** ## Methods ### track() ```ts wl.track(event: TrackEvent): Promise ``` Track a single event. Auto-generates `insert_id` (`randomUUID`) and `time` (`new Date().toISOString()`) if not provided. ```ts await wl.track({ event_type: "purchase", user_id: "u_123", event_properties: { plan: "pro", amount: 49 }, }); // { accepted: 1 } ``` ### trackBatch() ```ts wl.trackBatch(events: TrackEvent[]): Promise ``` Track up to 2000 events in one request. ```ts await wl.trackBatch([ { event_type: "page_view", user_id: "u_1", event_properties: { page: "/" } }, { event_type: "click", user_id: "u_1", event_properties: { button: "cta" } }, ]); // { accepted: 2 } ``` ### query() ```ts wl.query(q: string, opts?: QueryOptions): Promise ``` Run a pipe DSL query. Returns a Markdown string (`llm`), parsed JSON (`json`), or CSV string (`csv`). ```ts // Markdown table (default) const md = await wl.query("* | last 30d | count by event_type | top 10"); // JSON const data = await wl.query("page_view | last 7d | count", { format: "json" }); // CSV with pagination const csv = await wl.query("* | last 90d | count by event_type", { format: "csv", limit: 10000, }); ``` ### identify() ```ts wl.identify(params: IdentifyParams): Promise ``` Bind `device_id` to `user_id` and upsert user profile properties. No event emitted. ```ts await wl.identify({ user_id: "alice@acme.org", device_id: "dev_abc", user_properties: { email: "alice@acme.org" }, user_property_ops: { $set: { plan: "enterprise" }, $set_once: { signup_source: "ads" }, $add: { login_count: 1 }, $unset: ["legacy_flag"], }, }); // { ok: true } ``` *** ## Error handling Non-2xx responses throw `WireLogError`. ```ts import { WireLog, WireLogError } from "wirelog"; const wl = new WireLog(); try { await wl.track({ event_type: "test" }); } catch (e) { if (e instanceof WireLogError) { console.error(e.status); // HTTP status code console.error(e.message); // "WireLog API 401: ..." } } ``` *** ## Source [github.com/wirelogai/wirelog-js](https://github.com/wirelogai/wirelog-js) # Python Client > Zero-dependency Python analytics client. Zero external dependencies. Uses only Python stdlib (`urllib.request`, `json`, `time`, `uuid`, `os`). Python 3.9+. ## Install ```bash pip install wirelog ``` ## Quick start ```python from wirelog import WireLog wl = WireLog(api_key="sk_your_secret_key", host="https://yourhost.com") # Track an event wl.track("page_view", user_id="alice", event_properties={"page": "/pricing"}) # Query result = wl.query("page_view | last 7d | count by user_id") print(result) # Markdown table ``` *** ## Constructor ```python WireLog(api_key=None, host=None, timeout=30) ``` | Parameter | Type | Default | Notes | | --------- | ------------- | -------------------------------------------------- | ----------------------- | | `api_key` | `str \| None` | `WIRELOG_API_KEY` env var | `pk_`, `sk_`, or `aat_` | | `host` | `str \| None` | `WIRELOG_HOST` env var or `https://api.wirelog.ai` | API base URL | | `timeout` | `int` | `30` | HTTP timeout in seconds | *** ## Methods ### track() ```python wl.track( event_type, *, user_id=None, device_id=None, session_id=None, event_properties=None, user_properties=None, insert_id=None, ) ``` Returns `{"accepted": 1}`. Auto-generates `insert_id` (UUID hex) and `time` (UTC ISO 8601) if not provided. ```python wl.track( "purchase", user_id="u_123", event_properties={"plan": "pro", "amount": 49}, ) ``` ### track\_batch() ```python wl.track_batch(events) ``` `events` is a list of dicts matching the single-event schema. Returns `{"accepted": N}`. ```python wl.track_batch([ {"event_type": "page_view", "user_id": "u_1", "event_properties": {"page": "/"}}, {"event_type": "click", "user_id": "u_1", "event_properties": {"button": "cta"}}, ]) ``` ### query() ```python wl.query(q, *, format="llm", limit=100, offset=0) ``` | Parameter | Type | Default | Notes | | --------- | ----- | ------- | ------------------------------------- | | `q` | `str` | | Pipe DSL query string | | `format` | `str` | `"llm"` | `"llm"` (Markdown), `"json"`, `"csv"` | | `limit` | `int` | `100` | Max 10,000 | | `offset` | `int` | `0` | Pagination offset | Returns a Markdown string (`llm`), parsed JSON list (`json`), or CSV string (`csv`). ```python # Markdown table print(wl.query("* | last 30d | count by event_type | top 10")) # JSON data = wl.query("page_view | last 7d | count", format="json") # CSV export csv = wl.query("* | last 90d | count by event_type", format="csv", limit=10000) ``` ### identify() ```python wl.identify( user_id, *, device_id=None, user_properties=None, user_property_ops=None, ) ``` Returns `{"ok": True}`. Binds `device_id` to `user_id` and upserts user profile properties. No event emitted. ```python wl.identify( "alice@acme.org", device_id="dev_abc", user_properties={"email": "alice@acme.org"}, user_property_ops={ "$set": {"plan": "enterprise"}, "$set_once": {"signup_source": "ads"}, "$add": {"login_count": 1}, "$unset": ["legacy_flag"], }, ) ``` *** ## Error handling Non-2xx responses raise `WireLogError`. ```python from wirelog import WireLog, WireLogError wl = WireLog() try: wl.track("event") except WireLogError as e: print(e.status) # HTTP status code print(e) # "WireLog API 401: {"error":"missing api key"}" ``` *** ## Source [github.com/wirelogai/wirelog-python](https://github.com/wirelogai/wirelog-python)