diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f326fa2..6ad6d847 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Dashboard — self-hosted (enterprise) mount readiness + +The engine can now be mounted in any Rails app as the self-hosted +observability surface (see `docs/framework/self-hosted-observability.md`): + +- **One install generator**: the duplicate `active_agent:dashboard:install` + variant that copied eight migrations (agents, sandboxes, recordings — + tables for models with no shipped controllers or routes) is removed. + The surviving generator installs the telemetry traces table only and + gains `--skip_migrations` / `--skip_routes`; its initializer template now + covers authentication, `ingest_api_key`, and multi-tenant options. +- **Canonical mount path is `/activeagents`** (generator, dummy app and + docs updated), and the telemetry client now derives its local ingest + endpoint from wherever the engine is actually mounted — any mount path, + including `/` on a dedicated subdomain, works. + `Telemetry::Configuration::LOCAL_ENDPOINT_PATH` remains as the fallback + when the engine isn't mounted. +- **`TracesController` honors configuration**: index/metrics/time-series + queries now go through `ActiveAgent::Dashboard.trace_model` (previously + only `show` did) and are scoped with `for_account(current_owner)`, so a + `trace_model_class` override and multi-tenant scoping apply everywhere. +- **Single-tenant ingest auth**: new `config.ingest_api_key` requires a + matching Bearer token on `POST /api/traces` when set. The + telemetry reporter and ruby_llm_telemetry already send their `api_key` + as a Bearer header, so remote apps need no changes. +- **Metrics page no longer 500s with data**: the per-agent stats table + read a grouped SQL alias through a model method that expected per-trace + token columns. +- Removed the never-consumed `base_controller_class` config attribute. + ### Dashboard & Telemetry — dev console readiness The dashboard engine — Active Agent's local dev console — now works out of diff --git a/Gemfile b/Gemfile index b51157e6..1c5df889 100644 --- a/Gemfile +++ b/Gemfile @@ -3,8 +3,4 @@ source "https://rubygems.org" gem "debug" unless ENV["CI"] == "true" gem "rubocop-rails-omakase" -# Until activeagents-telemetry is published to RubyGems, resolve the gemspec -# dependency from GitHub. Remove this line after the first gem push. -gem "activeagents-telemetry", github: "activeagents/activeagents-telemetry" - gemspec diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index cf41e49a..e4837d50 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -100,6 +100,8 @@ export default defineConfig({ { text: 'Configuration', link: '/framework/configuration' }, { text: 'Instrumentation', link: '/framework/instrumentation' }, { text: 'Telemetry', link: '/framework/telemetry' }, + { text: 'Dashboard (Dev Console)', link: '/framework/dashboard' }, + { text: 'Self-Hosted Observability', link: '/framework/self-hosted-observability' }, { text: 'Retries', link: '/framework/retries' }, { text: 'Rails Integration', link: '/framework/rails' }, { text: 'Testing', link: '/framework/testing' }, diff --git a/docs/framework/dashboard.md b/docs/framework/dashboard.md index 95aaff06..3c174bdf 100644 --- a/docs/framework/dashboard.md +++ b/docs/framework/dashboard.md @@ -21,11 +21,15 @@ rails db:migrate The generator: -- copies the `active_agent_telemetry_traces` migration (plus agent, - run, template, sandbox and recording tables for the full install), -- mounts the engine at `/active_agent`, +- copies the `active_agent_telemetry_traces` migration (the one table the + dashboard reads), +- mounts the engine at `/activeagents`, - writes `config/initializers/active_agent_dashboard.rb`. +Deploying this beyond your laptop — for a team, or as the trace sink for +a fleet of apps? See +[Self-Hosted Observability](/framework/self-hosted-observability). + Then enable telemetry with local storage in `config/active_agent.yml`: ```yaml @@ -34,7 +38,7 @@ telemetry: local_storage: true ``` -That's it. Run any agent and open `/active_agent` — each generation +That's it. Run any agent and open `/activeagents` — each generation appears as a trace with prompt/LLM/tool spans, timing, token usage (input / output / thinking), provider and model. @@ -42,10 +46,10 @@ appears as a trace with prompt/LLM/tool spans, timing, token usage | Page | Path | Contents | |------|------|----------| -| Traces | `/active_agent/traces` | Every generation: agent + action, status, duration, tokens; expandable span timeline; All/Errors filter; 30s auto-refresh | -| Trace detail | `/active_agent/traces/:id` | Span waterfall with relative offsets, token breakdown, error details, raw payload | -| Metrics | `/active_agent/traces/metrics` | Last-24h totals: traces, tokens, avg duration, error rate, active agents; per-agent statistics | -| Ingest API | `POST /active_agent/api/traces` | JSON trace ingestion (used by `local_storage` mode and remote SDKs) | +| Traces | `/activeagents/traces` | Every generation: agent + action, status, duration, tokens; expandable span timeline; All/Errors filter; 30s auto-refresh | +| Trace detail | `/activeagents/traces/:id` | Span waterfall with relative offsets, token breakdown, error details, raw payload | +| Metrics | `/activeagents/traces/metrics` | Last-24h totals: traces, tokens, avg duration, error rate, active agents; per-agent statistics | +| Ingest API | `POST /activeagents/api/traces` | JSON trace ingestion (used by `local_storage` mode and remote SDKs) | Time-series charts on the metrics page use the optional [groupdate](https://github.com/ankane/groupdate) gem when present and @@ -70,13 +74,17 @@ Or constrain the mount in `config/routes.rb`: ```ruby authenticate :user, ->(u) { u.admin? } do - mount ActiveAgent::Dashboard::Engine => "/active_agent" + mount ActiveAgent::Dashboard::Engine => "/activeagents" end ``` -The local ingest endpoint is unauthenticated in local mode by design (it -receives traces from your own app process). In multi-tenant mode it -requires a Bearer token (see below). +The local ingest endpoint accepts unauthenticated posts by default (it +receives traces from your own app process on your own machine). If the +mount is reachable from other machines, set `config.ingest_api_key` to +require a Bearer token — see +[Self-Hosted Observability](/framework/self-hosted-observability). In +multi-tenant mode ingest always authenticates per-account keys (see +below). ## Sending traces to a remote endpoint instead diff --git a/docs/framework/self-hosted-observability.md b/docs/framework/self-hosted-observability.md new file mode 100644 index 00000000..69e39521 --- /dev/null +++ b/docs/framework/self-hosted-observability.md @@ -0,0 +1,225 @@ +# Self-Hosted Observability (Enterprise) + +Run the Active Agent dashboard inside your own Rails app: traces, span +waterfalls and metrics served from your database, on your domain. Data +never leaves your infrastructure. + +This is the same engine that powers the [dev console](/framework/dashboard) +— this guide covers deploying it as a shared, production observability +surface for a team or a fleet of apps. If you just want traces while you +build on your laptop, the dev console quick start is all you need. + +## Two ways to run observability + +| | Self-hosted engine | Hosted platform (activeagents.ai) | +|---|---|---| +| Where data lives | Your database | Your workspace on the platform | +| Setup | Mount the engine in a Rails app | Point telemetry at an API key | +| Traces + span waterfall | ✓ | ✓ | +| Metrics (24h aggregates, per-agent stats) | ✓ | ✓ | +| Ingest API for remote apps | ✓ (`/api/traces`) | ✓ (`https://api.activeagents.ai/v1/traces`) | +| Interactions (tool-call conversations), evaluations, scorecards, cost estimates | — | ✓ | +| Retention policies, team workspaces, plans | Yours to operate | ✓ | + +The wire format is identical, so the choice is per-environment, not +per-app: the same `config/active_agent.yml` switches between them (see +[Getting traces in](#getting-traces-in)). + +## Install + +```ruby +# Gemfile +gem "activeagent" +``` + +```bash +bundle install +rails generate active_agent:dashboard:install +rails db:migrate +``` + +The generator creates exactly three things: + +- `db/migrate/*_create_active_agent_telemetry_traces.rb` — the one table + the dashboard reads, +- `mount ActiveAgent::Dashboard::Engine => "/activeagents"` in + `config/routes.rb`, +- `config/initializers/active_agent_dashboard.rb` — authentication, + ingest key and multi-tenant options, commented. + +Open `http://localhost:3000/activeagents` and you have the dashboard. +(`--skip_migrations` / `--skip_routes` are available if you manage either +yourself.) + +## Routing: a path or a subdomain + +The mount path is yours to choose — the telemetry client derives its +local ingest endpoint from wherever the engine is actually mounted, so +nothing else needs configuring: + +```ruby +# A path on your main app: +mount ActiveAgent::Dashboard::Engine => "/activeagents" + +# Or the root of a dedicated subdomain, e.g. activeagents.combinaut.com: +constraints subdomain: "activeagents" do + mount ActiveAgent::Dashboard::Engine => "/", as: :active_agent_subdomain +end +``` + +With the subdomain mount, the dashboard lives at +`https://activeagents.combinaut.com/` and remote apps post traces to +`https://activeagents.combinaut.com/api/traces`. + +## Authentication (required in production) + +Traces contain prompts, outputs and error messages. Without an +`authentication_method` the dashboard refuses to serve in production +(HTTP 403), so set one before deploying: + +```ruby +# config/initializers/active_agent_dashboard.rb +ActiveAgent::Dashboard.configure do |config| + # Basic auth: + config.authentication_method = ->(controller) { + controller.authenticate_or_request_with_http_basic do |username, password| + username == "ops" && password == Rails.application.credentials.dashboard_password + end + } + # ...or Devise: ->(controller) { controller.authenticate_admin! } +end +``` + +The ingest API authenticates separately. In single-tenant mode it accepts +unauthenticated posts by default (fine for same-app `local_storage`, not +for a network-reachable mount) — set an ingest key whenever other +machines can reach it: + +```ruby +config.ingest_api_key = Rails.application.credentials.dig(:active_agent, :ingest_api_key) +``` + +Requests without a matching `Authorization: Bearer ` header get a +401. The telemetry reporter and `ruby_llm_telemetry` already send their +configured `api_key` as a Bearer header, so remote apps need no changes. + +## Getting traces in + +**Same app** — the app that mounts the dashboard stores its own traces +directly, no HTTP involved: + +```yaml +# config/active_agent.yml +production: + telemetry: + enabled: true + local_storage: true +``` + +**Other ActiveAgent apps in your fleet** — point their telemetry at your +mount: + +```yaml +production: + telemetry: + enabled: true + endpoint: https://activeagents.combinaut.com/api/traces + api_key: <%= Rails.application.credentials.dig(:active_agent, :ingest_api_key) %> +``` + +**The hosted platform instead** — same file, different endpoint; this is +what "cloud mode" is: + +```yaml +production: + telemetry: + enabled: true + endpoint: https://api.activeagents.ai/v1/traces + api_key: <%= ENV["ACTIVEAGENTS_API_KEY"] %> +``` + +**Multi-tenant mode** — if your self-hosted install itself serves multiple +accounts, enable `config.multi_tenant` with `account_class` / +`current_account_method`; ingest then authenticates per-account +`telemetry_api_key` Bearer tokens and processes asynchronously via +`ActiveAgent::ProcessTelemetryTracesJob` (requires an Active Job backend), +and every dashboard query scopes to the current account. Most self-hosted +installs should leave this off. + +## RubyLLM applications + +Apps that use [RubyLLM](https://rubyllm.com) directly (no +`ActiveAgent::Base`) can report chats and tool calls to the same endpoint +with the +[activeagents-telemetry-ruby_llm](https://rubygems.org/gems/activeagents-telemetry-ruby_llm) +adapter: + +```ruby +# Gemfile +gem "activeagents-telemetry-ruby_llm" +``` + +```ruby +# config/initializers/ruby_llm_telemetry.rb +ActiveAgents::Telemetry::RubyLLM.subscribe!( + endpoint: "https://activeagents.combinaut.com/api/traces", + api_key: Rails.application.credentials.dig(:active_agent, :ingest_api_key), + service_name: "billing-app" +) +``` + +Attribute traffic to named agents with `with_agent("SupportAgent", action: "respond") { ... }` +or an `agent_resolver:` lambda — otherwise traffic reports as +`RubyLLM::Chat`. See the bridge's README for content capture +(off by default) and turn semantics. + +## Optional: conversation persistence with solid_agent + +Telemetry gives you traces; [solid_agent](https://github.com/activeagents/solid_agent) +additionally persists conversations (contexts, messages, generations — +including tool calls with arguments and results) in your database: + +```bash +rails generate solid_agent:install +rails db:migrate +``` + +Generations record the same `trace_id` the telemetry pipeline uses +(thread it via `prompt_options[:trace_id]`), so conversation rows and +dashboard traces correlate. + +## Operations + +- **Retention is yours.** Nothing prunes automatically. A recurring job + as simple as + `ActiveAgent::TelemetryTrace.where("created_at < ?", 30.days.ago).delete_all` + is enough. +- **Database portability.** The engine's queries are plain ActiveRecord — + SQLite, MySQL and PostgreSQL all work. (The hosted platform's richer + views use PostgreSQL-specific SQL; the engine deliberately doesn't.) +- **CDN assets.** The default layout loads Tailwind, Turbo and Stimulus + from public CDNs. On CSP-strict or air-gapped networks the pages render + unstyled (fully functional, but plain) — set `config.layout` to a + layout of your own that bundles those assets locally. +- **Time-series charts** on the metrics page light up when the optional + [groupdate](https://github.com/ankane/groupdate) gem is installed. +- **Sensitive content.** Prompt/output capture obeys the telemetry + `redact_attributes` configuration — see [Telemetry](/framework/telemetry). + +## Troubleshooting + +- **No traces appear** — telemetry is opt-in per environment: check + `enabled: true` (and `local_storage: true` for same-app storage) under + the *current* environment key in `config/active_agent.yml`. +- **403 in production** — set `config.authentication_method` (see above). +- **401 from ingest** — the poster's `api_key` doesn't match + `config.ingest_api_key` (single-tenant) or an account + `telemetry_api_key` (multi-tenant). +- **Metrics page has no chart** — install `groupdate`. + +## Future work + +Rendering agent conversations persisted by RubyLLM's `acts_as` schema or +solid_agent tables directly in the engine (DB-level detection of agent +implementations) is on the roadmap; today those render on the hosted +platform via telemetry. diff --git a/docs/framework/telemetry.md b/docs/framework/telemetry.md index 296ab674..3725adfe 100644 --- a/docs/framework/telemetry.md +++ b/docs/framework/telemetry.md @@ -215,6 +215,12 @@ at_exit { ActiveAgent::Telemetry.shutdown } ## Self-Hosting +The dashboard engine already implements this endpoint spec: mount it and +its ingest API (`/api/traces`) receives traces from any app in +your fleet — see +[Self-Hosted Observability](/framework/self-hosted-observability). The +requirements below are for building your own receiver instead. + ### Endpoint Requirements Your telemetry endpoint must accept POST requests with: diff --git a/gemfiles/anthropic_1.12.gemfile b/gemfiles/anthropic_1.12.gemfile index 2bd561d3..10f9d3c5 100644 --- a/gemfiles/anthropic_1.12.gemfile +++ b/gemfiles/anthropic_1.12.gemfile @@ -8,7 +8,3 @@ gem "rails", "~> 8.0.0" gem "sqlite3", "~> 2.0" gemspec path: ".." - -# Until activeagents-telemetry is published to RubyGems, resolve the gemspec -# dependency from GitHub. Remove after the first gem push. -gem "activeagents-telemetry", github: "activeagents/activeagents-telemetry" diff --git a/gemfiles/anthropic_1.14.gemfile b/gemfiles/anthropic_1.14.gemfile index b4b8586d..04258a6f 100644 --- a/gemfiles/anthropic_1.14.gemfile +++ b/gemfiles/anthropic_1.14.gemfile @@ -8,7 +8,3 @@ gem "rails", "~> 8.0.0" gem "sqlite3", "~> 2.0" gemspec path: ".." - -# Until activeagents-telemetry is published to RubyGems, resolve the gemspec -# dependency from GitHub. Remove after the first gem push. -gem "activeagents-telemetry", github: "activeagents/activeagents-telemetry" diff --git a/gemfiles/anthropic_latest.gemfile b/gemfiles/anthropic_latest.gemfile index 6858b605..7cd5277f 100644 --- a/gemfiles/anthropic_latest.gemfile +++ b/gemfiles/anthropic_latest.gemfile @@ -6,7 +6,3 @@ gem "rails", "~> 8.0.0" gem "sqlite3", "~> 2.0" gemspec path: ".." - -# Until activeagents-telemetry is published to RubyGems, resolve the gemspec -# dependency from GitHub. Remove after the first gem push. -gem "activeagents-telemetry", github: "activeagents/activeagents-telemetry" diff --git a/gemfiles/openai_0.34.gemfile b/gemfiles/openai_0.34.gemfile index 4c606566..fcca991b 100644 --- a/gemfiles/openai_0.34.gemfile +++ b/gemfiles/openai_0.34.gemfile @@ -8,7 +8,3 @@ gem "rails", "~> 8.0.0" gem "sqlite3", "~> 2.0" gemspec path: ".." - -# Until activeagents-telemetry is published to RubyGems, resolve the gemspec -# dependency from GitHub. Remove after the first gem push. -gem "activeagents-telemetry", github: "activeagents/activeagents-telemetry" diff --git a/gemfiles/openai_0.35.gemfile b/gemfiles/openai_0.35.gemfile index e01c1768..e1669b07 100644 --- a/gemfiles/openai_0.35.gemfile +++ b/gemfiles/openai_0.35.gemfile @@ -8,7 +8,3 @@ gem "rails", "~> 8.0.0" gem "sqlite3", "~> 2.0" gemspec path: ".." - -# Until activeagents-telemetry is published to RubyGems, resolve the gemspec -# dependency from GitHub. Remove after the first gem push. -gem "activeagents-telemetry", github: "activeagents/activeagents-telemetry" diff --git a/gemfiles/openai_latest.gemfile b/gemfiles/openai_latest.gemfile index fa85c16a..b9d7cd66 100644 --- a/gemfiles/openai_latest.gemfile +++ b/gemfiles/openai_latest.gemfile @@ -6,7 +6,3 @@ gem "rails", "~> 8.0.0" gem "sqlite3", "~> 2.0" gemspec path: ".." - -# Until activeagents-telemetry is published to RubyGems, resolve the gemspec -# dependency from GitHub. Remove after the first gem push. -gem "activeagents-telemetry", github: "activeagents/activeagents-telemetry" diff --git a/gemfiles/rails7.gemfile b/gemfiles/rails7.gemfile index 8205e1cd..fa45e8d5 100644 --- a/gemfiles/rails7.gemfile +++ b/gemfiles/rails7.gemfile @@ -5,7 +5,3 @@ gem "sqlite3", "~> 1.4" gem "rails", "~> 7.0" gemspec path: ".." - -# Until activeagents-telemetry is published to RubyGems, resolve the gemspec -# dependency from GitHub. Remove after the first gem push. -gem "activeagents-telemetry", github: "activeagents/activeagents-telemetry" diff --git a/gemfiles/rails8.gemfile b/gemfiles/rails8.gemfile index 96474847..bfee8d5d 100644 --- a/gemfiles/rails8.gemfile +++ b/gemfiles/rails8.gemfile @@ -6,7 +6,3 @@ gem "rails", "~> 8.1.1" gem "tzinfo-data" gemspec path: ".." - -# Until activeagents-telemetry is published to RubyGems, resolve the gemspec -# dependency from GitHub. Remove after the first gem push. -gem "activeagents-telemetry", github: "activeagents/activeagents-telemetry" diff --git a/gemfiles/railsmain.gemfile b/gemfiles/railsmain.gemfile index 2764c76b..e265d0c3 100644 --- a/gemfiles/railsmain.gemfile +++ b/gemfiles/railsmain.gemfile @@ -5,7 +5,3 @@ gem "sqlite3", "~> 2.0" gem "rails", github: "rails/rails" gemspec path: ".." - -# Until activeagents-telemetry is published to RubyGems, resolve the gemspec -# dependency from GitHub. Remove after the first gem push. -gem "activeagents-telemetry", github: "activeagents/activeagents-telemetry" diff --git a/lib/active_agent/dashboard.rb b/lib/active_agent/dashboard.rb index b7977661..4fcdee9f 100644 --- a/lib/active_agent/dashboard.rb +++ b/lib/active_agent/dashboard.rb @@ -8,7 +8,7 @@ module ActiveAgent # Mount the engine in your routes to access the full dashboard: # # # config/routes.rb - # mount ActiveAgent::Dashboard::Engine => "/active_agent" + # mount ActiveAgent::Dashboard::Engine => "/activeagents" # # The dashboard provides: # - Agent management: Create, edit, version, and execute agents @@ -91,9 +91,12 @@ class << self # @return [Object, nil] Object responding to #signed_url_for and #fetch_snapshot attr_accessor :storage_service - # Base controller class for dashboard controllers - # @return [String] - attr_accessor :base_controller_class + # Bearer token required by the ingest API in single-tenant mode. When + # unset the local ingest endpoint accepts unauthenticated posts, so set + # it whenever the mount is reachable beyond your own machine. + # (Multi-tenant mode authenticates per-account keys instead.) + # @return [String, nil] + attr_accessor :ingest_api_key # Returns whether multi-tenant mode is enabled. # @@ -141,7 +144,7 @@ def reset! @sandbox_service = :local @sandbox_limits = nil @storage_service = nil - @base_controller_class = "ActionController::Base" + @ingest_api_key = nil end end diff --git a/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb b/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb index d58325ad..db33f4e5 100644 --- a/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb +++ b/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb @@ -9,8 +9,11 @@ module Api # for analysis and visualization in the dashboard. # # Supports two modes: - # - Local mode: No authentication, synchronous processing - # - Multi-tenant mode: Bearer token auth, async processing via job + # - Local mode: synchronous processing; unauthenticated unless + # ActiveAgent::Dashboard.ingest_api_key is set (set it whenever the + # mount is reachable beyond your own machine) + # - Multi-tenant mode: per-account Bearer token auth, async processing + # via job # # @example Local mode request # POST /active_agent/api/traces @@ -33,6 +36,7 @@ module Api # class TracesController < ActionController::API before_action :authenticate_api_key!, if: -> { ActiveAgent::Dashboard.multi_tenant? } + before_action :authenticate_ingest_key!, unless: -> { ActiveAgent::Dashboard.multi_tenant? } # Maximum traces accepted per request (mirrors # ProcessTelemetryTracesJob::MAX_TRACES_PER_JOB). @@ -90,6 +94,19 @@ def authenticate_api_key! @account.increment_telemetry_usage! if @account.respond_to?(:increment_telemetry_usage!) end + # Requires the configured single-tenant ingest key when one is set. + # The telemetry reporter and ruby_llm_telemetry both send their + # api_key as a Bearer header, so remote apps work unchanged. + def authenticate_ingest_key! + expected = ActiveAgent::Dashboard.ingest_api_key + return if expected.blank? + + token = extract_bearer_token + return if token.present? && ActiveSupport::SecurityUtils.secure_compare(token, expected) + + render json: { error: "Invalid API key" }, status: :unauthorized + end + # Extracts Bearer token from Authorization header. def extract_bearer_token auth_header = request.headers["Authorization"] diff --git a/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb b/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb index 26d0b19a..66f21431 100644 --- a/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb +++ b/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb @@ -23,7 +23,7 @@ def index end def show - @trace = ActiveAgent::Dashboard.trace_model.find(params[:id]) + @trace = scoped_traces.find(params[:id]) end def metrics @@ -43,8 +43,14 @@ def turbo_stream_available? Mime::Type.lookup_by_extension(:turbo_stream).present? end + # All queries honor the configured trace model and, in multi-tenant + # mode, the current owner's account (for_account no-ops otherwise). + def scoped_traces + ActiveAgent::Dashboard.trace_model.for_account(current_owner) + end + def fetch_traces - traces = ActiveAgent::TelemetryTrace.recent + traces = scoped_traces.recent traces = traces.for_agent(params[:agent]) if params[:agent].present? traces = traces.with_errors if params[:status] == "error" @@ -61,7 +67,7 @@ def fetch_traces end def calculate_metrics - traces = ActiveAgent::TelemetryTrace.where( + traces = scoped_traces.where( "created_at > ?", 24.hours.ago ) @@ -83,7 +89,7 @@ def calculate_error_rate(traces) end def agent_statistics - ActiveAgent::TelemetryTrace + scoped_traces .where("created_at > ?", 24.hours.ago) .group(:agent_class) .select( @@ -96,7 +102,7 @@ def agent_statistics end def time_series_data - ActiveAgent::TelemetryTrace + scoped_traces .where("created_at > ?", 1.hour.ago) .group_by_minute(:created_at) .count diff --git a/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb b/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb index 9b05bf0f..072b5503 100644 --- a/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb +++ b/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb @@ -114,7 +114,9 @@ <%= number_with_delimiter(stat.trace_count) %> - <%= number_to_human(stat.total_tokens || 0, precision: 1) %> + <%# stat is a grouped row: read the SQL alias, not the model + method (which sums per-trace token columns absent here) %> + <%= number_to_human(stat[:total_tokens] || 0, precision: 1) %> <% avg = stat.avg_duration.to_f %> diff --git a/lib/active_agent/dashboard/engine.rb b/lib/active_agent/dashboard/engine.rb index c313e846..13fb0a03 100644 --- a/lib/active_agent/dashboard/engine.rb +++ b/lib/active_agent/dashboard/engine.rb @@ -8,7 +8,7 @@ module Dashboard # and the local trace ingestion API. # # Mount in your routes: - # mount ActiveAgent::Dashboard::Engine => "/active_agent" + # mount ActiveAgent::Dashboard::Engine => "/activeagents" # class Engine < ::Rails::Engine # The engine lives at lib/active_agent/dashboard rather than the gem diff --git a/lib/active_agent/telemetry/configuration.rb b/lib/active_agent/telemetry/configuration.rb index 1fed40b3..faf934e5 100644 --- a/lib/active_agent/telemetry/configuration.rb +++ b/lib/active_agent/telemetry/configuration.rb @@ -17,8 +17,9 @@ module Telemetry # # @see ActiveAgents::Telemetry::Configuration class Configuration < ActiveAgents::Telemetry::Configuration - # Local dashboard endpoint path (relative to app root) - LOCAL_ENDPOINT_PATH = "/active_agent/api/traces" + # Fallback ingest path when the dashboard engine's mount point can't + # be resolved from the host's routes (e.g. engine not mounted). + LOCAL_ENDPOINT_PATH = "/activeagents/api/traces" # @return [Boolean] Whether to store traces in the app's own database attr_reader :local_storage @@ -51,7 +52,16 @@ def local_store # Returns the resolved endpoint for trace reporting. def resolved_endpoint - local_storage? ? LOCAL_ENDPOINT_PATH : endpoint + local_storage? ? local_endpoint_path : endpoint + end + + # The dashboard engine's ingest path, derived from wherever the host + # app actually mounted it — "/activeagents", "/observability", or "/" + # on a dedicated subdomain all work. Falls back to + # LOCAL_ENDPOINT_PATH when the engine isn't mounted. + def local_endpoint_path + mount = dashboard_mount_path + mount ? "#{mount}/api/traces" : LOCAL_ENDPOINT_PATH end # The framework's historical fallback is "activeagent", not the shared @@ -96,6 +106,17 @@ def local_trace_model rescue NameError nil end + + # The engine's mount point in the host app, via the mount helper Rails + # defines from the engine_name ("active_agent"). Returns nil when the + # engine isn't mounted or no Rails app is booted; "" for a root mount. + def dashboard_mount_path + return nil unless defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application + + ::Rails.application.routes.url_helpers.active_agent_path.chomp("/") + rescue NoMethodError, NameError + nil + end end end end diff --git a/lib/generators/active_agent/dashboard/install/install_generator.rb b/lib/generators/active_agent/dashboard/install/install_generator.rb deleted file mode 100644 index 8b3ea015..00000000 --- a/lib/generators/active_agent/dashboard/install/install_generator.rb +++ /dev/null @@ -1,95 +0,0 @@ -# frozen_string_literal: true - -require "rails/generators" -require "rails/generators/active_record" - -module ActiveAgent - module Dashboard - module Generators - # Generator for installing the ActiveAgent Dashboard engine. - # - # Usage: - # rails generate active_agent:dashboard:install - # - # This will: - # - Copy migration files for all dashboard models - # - Create an initializer for configuration - # - Add the engine mount to routes.rb - # - Seed default agent templates - # - class InstallGenerator < Rails::Generators::Base - include ActiveRecord::Generators::Migration - - source_root File.expand_path("templates", __dir__) - - class_option :multi_tenant, type: :boolean, default: false, - desc: "Configure for multi-tenant mode with account association" - - class_option :skip_migrations, type: :boolean, default: false, - desc: "Skip copying migration files" - - class_option :skip_routes, type: :boolean, default: false, - desc: "Skip adding route mount" - - def copy_migrations - return if options[:skip_migrations] - - migration_template "migrations/create_active_agent_agents.rb", - "db/migrate/create_active_agent_agents.rb" - - migration_template "migrations/create_active_agent_agent_versions.rb", - "db/migrate/create_active_agent_agent_versions.rb" - - migration_template "migrations/create_active_agent_agent_runs.rb", - "db/migrate/create_active_agent_agent_runs.rb" - - migration_template "migrations/create_active_agent_agent_templates.rb", - "db/migrate/create_active_agent_agent_templates.rb" - - migration_template "migrations/create_active_agent_sandbox_sessions.rb", - "db/migrate/create_active_agent_sandbox_sessions.rb" - - migration_template "migrations/create_active_agent_sandbox_runs.rb", - "db/migrate/create_active_agent_sandbox_runs.rb" - - migration_template "migrations/create_active_agent_session_recordings.rb", - "db/migrate/create_active_agent_session_recordings.rb" - - migration_template "migrations/create_active_agent_telemetry_traces.rb", - "db/migrate/create_active_agent_telemetry_traces.rb" - end - - def create_initializer - template "initializer.rb", "config/initializers/active_agent_dashboard.rb" - end - - def mount_engine - return if options[:skip_routes] - - route 'mount ActiveAgent::Dashboard::Engine => "/active_agent"' - end - - def show_post_install - say "" - say "ActiveAgent Dashboard installed!", :green - say "" - say "Next steps:" - say " 1. Run migrations: rails db:migrate" - say " 3. Configure authentication in config/initializers/active_agent_dashboard.rb" - say " 4. Visit /active_agent to access the dashboard" - say "" - end - - private - - def migration_version - "[#{ActiveRecord::Migration.current_version}]" - end - - def multi_tenant? - options[:multi_tenant] - end - end - end - end -end diff --git a/lib/generators/active_agent/dashboard/install/templates/initializer.rb b/lib/generators/active_agent/dashboard/install/templates/initializer.rb deleted file mode 100644 index e8d3a279..00000000 --- a/lib/generators/active_agent/dashboard/install/templates/initializer.rb +++ /dev/null @@ -1,89 +0,0 @@ -# frozen_string_literal: true - -# ActiveAgent Dashboard Configuration -# -# This initializer configures the ActiveAgent Dashboard engine. -# See https://docs.activeagents.ai/dashboard for full documentation. - -ActiveAgent::Dashboard.configure do |config| - # ========================================================================== - # Authentication - # ========================================================================== - - # Set an authentication method that will be called on all dashboard controllers. - # This should authenticate the user and redirect/raise if unauthorized. - # - # Examples: - # config.authentication_method = ->(controller) { controller.authenticate_admin! } - # config.authentication_method = ->(controller) { controller.authenticate_user! } - # - # config.authentication_method = nil - -<% if multi_tenant? -%> - # ========================================================================== - # Multi-tenant Mode - # ========================================================================== - - # Enable multi-tenant mode for SaaS deployments with multiple accounts. - config.multi_tenant = true - - # The Account model class name - config.account_class = "Account" - - # The User model class name - config.user_class = "User" - - # Method to call on controllers to get the current account - config.current_account_method = :current_account - - # Method to call on controllers to get the current user - config.current_user_method = :current_user - -<% else -%> - # ========================================================================== - # Local Mode (default) - # ========================================================================== - - # Multi-tenant mode is disabled by default. - # Set to true if you're building a SaaS platform with multiple accounts. - # config.multi_tenant = false - - # Optional: Associate agents with users - # config.user_class = "User" - # config.current_user_method = :current_user - -<% end -%> - # ========================================================================== - # Sandbox Configuration - # ========================================================================== - - # Sandbox service type for agent execution environments. - # Options: :local (Docker/Incus), :cloud_run, :kubernetes - config.sandbox_service = :local - - # Custom sandbox limits (optional) - # config.sandbox_limits = { - # max_runs: 10, - # timeout_seconds: 300, - # max_tokens: 50_000, - # session_duration_minutes: 15 - # } - - # ========================================================================== - # UI Configuration - # ========================================================================== - - # Use Inertia.js with React for the frontend (requires additional setup) - # config.use_inertia = false - - # Custom layout for dashboard views - # config.layout = "application" - - # ========================================================================== - # Storage Configuration - # ========================================================================== - - # Storage service for screenshots and snapshots. - # Must respond to #signed_url_for(key, expires_in:) and #fetch_snapshot(key) - # config.storage_service = MyStorageService.new -end diff --git a/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_runs.rb b/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_runs.rb deleted file mode 100644 index 523a98ef..00000000 --- a/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_runs.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -class CreateActiveAgentAgentRuns < ActiveRecord::Migration<%= migration_version %> - def change - create_table :active_agent_agent_runs do |t| - t.references :agent, null: false, foreign_key: { to_table: :active_agent_agents } - - # Input - t.text :input_prompt - t.json :input_params, default: {} - - # Output - t.text :output - t.json :output_metadata, default: {} - - # Execution details - t.integer :status, default: 0, null: false - t.integer :duration_ms - t.datetime :started_at - t.datetime :completed_at - - # Token usage - t.integer :input_tokens - t.integer :output_tokens - t.integer :total_tokens - - # Error tracking - t.text :error_message - t.text :error_backtrace - - # Trace for debugging - t.string :trace_id - t.json :logs, default: [] - - t.timestamps - end - - add_index :active_agent_agent_runs, :status - add_index :active_agent_agent_runs, :trace_id - add_index :active_agent_agent_runs, :created_at - end -end diff --git a/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_templates.rb b/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_templates.rb deleted file mode 100644 index a969e924..00000000 --- a/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_templates.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -class CreateActiveAgentAgentTemplates < ActiveRecord::Migration<%= migration_version %> - def change - create_table :active_agent_agent_templates do |t| - t.string :name, null: false - t.string :slug, null: false - t.text :description - t.string :category - - # Template configuration (same as agents) - t.string :provider, default: "openai" - t.string :model, default: "gpt-4o-mini" - t.text :instructions - t.string :preset_type - t.json :appearance, default: {} - t.json :instruction_sets, default: [] - t.json :tools, default: [] - t.json :mcp_servers, default: {} - t.json :model_config, default: {} - - # Metadata - t.string :icon - t.integer :usage_count, default: 0 - t.boolean :featured, default: false - t.boolean :public, default: true - t.boolean :free_tier, default: true - - t.timestamps - end - - add_index :active_agent_agent_templates, :slug, unique: true - add_index :active_agent_agent_templates, :category - add_index :active_agent_agent_templates, :featured - add_index :active_agent_agent_templates, :usage_count - add_index :active_agent_agent_templates, :free_tier - end -end diff --git a/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_versions.rb b/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_versions.rb deleted file mode 100644 index a89ecccf..00000000 --- a/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_versions.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -class CreateActiveAgentAgentVersions < ActiveRecord::Migration<%= migration_version %> - def change - create_table :active_agent_agent_versions do |t| - t.references :agent, null: false, foreign_key: { to_table: :active_agent_agents } - - t.integer :version_number, null: false, default: 1 - t.string :change_summary - - # Snapshot of agent configuration at this version - t.json :configuration_snapshot, null: false, default: {} - - # Who made the change - t.string :created_by - - t.timestamps - end - - add_index :active_agent_agent_versions, [:agent_id, :version_number], unique: true - end -end diff --git a/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agents.rb b/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agents.rb deleted file mode 100644 index 37e5e32f..00000000 --- a/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agents.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -class CreateActiveAgentAgents < ActiveRecord::Migration<%= migration_version %> - def change - create_table :active_agent_agents do |t| - t.string :name, null: false - t.text :description - t.string :slug, null: false - - # Agent class configuration - t.string :agent_class_name - t.string :provider, default: "openai" - t.string :model, default: "gpt-4o-mini" - - # Instructions and system prompt - t.text :instructions - - # Avatar/appearance configuration - t.string :preset_type - t.json :appearance, default: {} - - # Capabilities - t.json :instruction_sets, default: [] - t.json :tools, default: [] - t.json :mcp_servers, default: [] - - # Model configuration - t.json :model_config, default: {} - - # Response format - t.json :response_format, default: {} - - # Status - t.integer :status, default: 0, null: false - - # Owner associations (optional) - t.references :user, foreign_key: true, null: true -<% if multi_tenant? -%> - t.references :account, foreign_key: true, null: true -<% end -%> - - t.timestamps - end - -<% if multi_tenant? -%> - add_index :active_agent_agents, [:account_id, :slug], unique: true -<% else -%> - add_index :active_agent_agents, [:user_id, :slug], unique: true -<% end -%> - add_index :active_agent_agents, :status - add_index :active_agent_agents, :provider - end -end diff --git a/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_runs.rb b/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_runs.rb deleted file mode 100644 index 69ed12ba..00000000 --- a/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_runs.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -class CreateActiveAgentSandboxRuns < ActiveRecord::Migration<%= migration_version %> - def change - create_table :active_agent_sandbox_runs do |t| - t.references :sandbox_session, foreign_key: { to_table: :active_agent_sandbox_sessions }, null: true - - t.text :task, null: false - t.integer :status, default: 0, null: false - - # Execution details - t.text :result - t.text :error - t.integer :duration_ms - t.integer :tokens_used - t.datetime :started_at - t.datetime :completed_at - - # Screenshots - t.json :screenshots, default: [] - - t.timestamps - end - - add_index :active_agent_sandbox_runs, :status - add_index :active_agent_sandbox_runs, :created_at - end -end diff --git a/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_sessions.rb b/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_sessions.rb deleted file mode 100644 index fe148db1..00000000 --- a/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_sessions.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -class CreateActiveAgentSandboxSessions < ActiveRecord::Migration<%= migration_version %> - def change - create_table :active_agent_sandbox_sessions do |t| - t.string :session_id, null: false - t.references :user, foreign_key: true, null: true -<% if multi_tenant? -%> - t.references :account, foreign_key: true, null: true -<% end -%> - t.references :agent_template, foreign_key: { to_table: :active_agent_agent_templates }, null: true - - # Session metadata - t.string :sandbox_type, default: "playwright_mcp" - t.integer :status, default: 0 - t.string :cloud_run_job_id - t.string :cloud_run_url - - # Execution tracking - t.integer :runs_count, default: 0 - t.integer :max_runs, default: 10 - t.integer :timeout_seconds, default: 300 - t.datetime :expires_at - t.datetime :last_activity_at - - # Resource usage - t.integer :total_tokens, default: 0 - t.integer :total_duration_ms, default: 0 - - # Results - t.json :runs, default: [] - t.text :error_message - - t.timestamps - end - - add_index :active_agent_sandbox_sessions, :session_id, unique: true - add_index :active_agent_sandbox_sessions, :status - add_index :active_agent_sandbox_sessions, :sandbox_type - add_index :active_agent_sandbox_sessions, :expires_at - add_index :active_agent_sandbox_sessions, :cloud_run_job_id - end -end diff --git a/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_session_recordings.rb b/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_session_recordings.rb deleted file mode 100644 index 747e7a79..00000000 --- a/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_session_recordings.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -class CreateActiveAgentSessionRecordings < ActiveRecord::Migration<%= migration_version %> - def change - create_table :active_agent_session_recordings do |t| - t.references :agent_run, null: true, foreign_key: { to_table: :active_agent_agent_runs } - t.references :sandbox_session, null: true, foreign_key: { to_table: :active_agent_sandbox_sessions } - t.string :name - t.integer :status, default: 0, null: false - t.integer :duration_ms - t.integer :action_count, default: 0 - t.json :metadata, default: {} - t.timestamps - end - - create_table :active_agent_recording_actions do |t| - t.references :session_recording, null: false, foreign_key: { to_table: :active_agent_session_recordings } - t.string :action_type, null: false - t.integer :sequence, null: false - t.integer :timestamp_ms, null: false - t.string :selector - t.text :value - t.string :screenshot_key - t.string :dom_snapshot_key - t.json :metadata, default: {} - t.timestamps - end - - add_index :active_agent_recording_actions, [:session_recording_id, :sequence], unique: true, name: "idx_recording_actions_on_recording_and_sequence" - - create_table :active_agent_recording_snapshots do |t| - t.references :session_recording, null: false, foreign_key: { to_table: :active_agent_session_recordings } - t.references :recording_action, null: true, foreign_key: { to_table: :active_agent_recording_actions } - t.string :storage_key, null: false - t.string :snapshot_type, null: false - t.integer :width - t.integer :height - t.integer :file_size_bytes - t.timestamps - end - - add_index :active_agent_recording_snapshots, :storage_key, unique: true - end -end diff --git a/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_telemetry_traces.rb b/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_telemetry_traces.rb deleted file mode 100644 index c974efe3..00000000 --- a/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_telemetry_traces.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true - -class CreateActiveAgentTelemetryTraces < ActiveRecord::Migration<%= migration_version %> - def change - create_table :active_agent_telemetry_traces do |t| -<% if multi_tenant? -%> - t.references :account, foreign_key: true, null: true -<% end -%> - - # Trace identification - t.string :trace_id, null: false - t.string :service_name - t.string :environment - - # Timing - t.datetime :timestamp, null: false - - # Span data (JSON array of spans) - t.json :spans, default: [] - - # Resource attributes - t.json :resource_attributes, default: {} - - # SDK info - t.json :sdk_info, default: {} - - # Aggregated metrics (for quick queries) - t.integer :total_duration_ms - t.integer :total_input_tokens, default: 0 - t.integer :total_output_tokens, default: 0 - t.integer :total_thinking_tokens, default: 0 - - # Status - t.string :status, default: "UNSET" - - # Agent info (denormalized for queries) - t.string :agent_class - t.string :agent_action - - # Error info - t.text :error_message - - t.timestamps - end - - add_index :active_agent_telemetry_traces, :trace_id, unique: true - add_index :active_agent_telemetry_traces, :timestamp - add_index :active_agent_telemetry_traces, :service_name - add_index :active_agent_telemetry_traces, :environment - add_index :active_agent_telemetry_traces, :agent_class - add_index :active_agent_telemetry_traces, :status -<% if multi_tenant? -%> - add_index :active_agent_telemetry_traces, [:account_id, :timestamp] -<% end -%> - end -end diff --git a/lib/generators/active_agent/dashboard/install_generator.rb b/lib/generators/active_agent/dashboard/install_generator.rb index db34f809..85dd6f55 100644 --- a/lib/generators/active_agent/dashboard/install_generator.rb +++ b/lib/generators/active_agent/dashboard/install_generator.rb @@ -22,7 +22,15 @@ class InstallGenerator < Rails::Generators::Base desc "Installs the ActiveAgent Dashboard with telemetry storage" + class_option :skip_migrations, type: :boolean, default: false, + desc: "Skip copying the telemetry traces migration" + + class_option :skip_routes, type: :boolean, default: false, + desc: "Skip adding the engine mount to routes.rb" + def copy_migrations + return if options[:skip_migrations] + migration_template( "create_active_agent_telemetry_traces.rb.erb", "db/migrate/create_active_agent_telemetry_traces.rb" @@ -30,7 +38,9 @@ def copy_migrations end def add_route - route 'mount ActiveAgent::Dashboard::Engine => "/active_agent"' + return if options[:skip_routes] + + route 'mount ActiveAgent::Dashboard::Engine => "/activeagents"' end def create_initializer @@ -50,7 +60,7 @@ def show_readme say " telemetry:" say " enabled: true" say " local_storage: true" - say " 3. Visit /active_agent to view the dashboard" + say " 3. Visit /activeagents to view the dashboard" say "\n" end diff --git a/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb b/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb index 02a41897..2987fff9 100644 --- a/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb +++ b/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb @@ -3,13 +3,17 @@ # ActiveAgent Dashboard Configuration # # This file configures the ActiveAgent telemetry dashboard. -# The dashboard is mounted at /active_agent by default. +# The dashboard is mounted at /activeagents by default (see config/routes.rb). ActiveAgent::Dashboard.configure do |config| - # Authentication method - provide a lambda that receives the controller - # and performs authentication. Return false or raise to deny access. + # ========================================================================== + # Authentication (required in production) + # ========================================================================== # - # Examples: + # Provide a lambda that receives the controller and performs + # authentication. Return false or raise to deny access. Without one, the + # dashboard serves a 403 in production — traces contain prompts, outputs + # and error messages. # # Basic auth: # config.authentication_method = ->(controller) { @@ -27,4 +31,37 @@ ActiveAgent::Dashboard.configure do |config| # config.authentication_method = nil # config.authentication_method = nil + + # ========================================================================== + # Ingest API authentication + # ========================================================================== + # + # Bearer token other apps must send when posting traces to the mounted + # ingest endpoint (/activeagents/api/traces). Leave unset only when the + # mount is not reachable beyond your own machine. + # + # config.ingest_api_key = Rails.application.credentials.dig(:active_agent, :ingest_api_key) + + # ========================================================================== + # Multi-tenant mode (SaaS platforms only) + # ========================================================================== + # + # Scopes traces per account and authenticates ingest with per-account + # keys. Most self-hosted installs should leave this off. + # + # config.multi_tenant = true + # config.account_class = "Account" + # config.user_class = "User" + # config.current_account_method = :current_account + # config.current_user_method = :current_user + + # ========================================================================== + # UI + # ========================================================================== + # + # Custom layout for dashboard views (the default layout loads Tailwind, + # Turbo and Stimulus from CDNs — override for CSP-strict or air-gapped + # environments). + # + # config.layout = "application" end diff --git a/test/dashboard/engine_integration_test.rb b/test/dashboard/engine_integration_test.rb index 87fe5e63..8ff0f9b2 100644 --- a/test/dashboard/engine_integration_test.rb +++ b/test/dashboard/engine_integration_test.rb @@ -39,40 +39,74 @@ def setup test "traces index renders" do ActiveAgent::TelemetryTrace.create_from_payload(sample_payload) - get "/active_agent/traces" + get "/activeagents/traces" assert_response :success assert_includes response.body, "SupportAgent" end + test "traces index and metrics honor a trace_model_class override" do + override = Class.new(ActiveAgent::TelemetryTrace) do + default_scope { where(service_name: "scoped-service") } + end + Object.const_set(:ScopedTelemetryTrace, override) + + ActiveAgent::TelemetryTrace.create_from_payload(sample_payload) + scoped = sample_payload + scoped["service_name"] = "scoped-service" + scoped["spans"][0]["name"] = "ScopedAgent.respond" + scoped["spans"][0]["attributes"]["agent.class"] = "ScopedAgent" + ActiveAgent::TelemetryTrace.create_from_payload(scoped) + + ActiveAgent::Dashboard.trace_model_class = "ScopedTelemetryTrace" + + get "/activeagents/traces" + + assert_response :success + assert_includes response.body, "ScopedAgent" + assert_not_includes response.body, "SupportAgent" + + get "/activeagents/traces/metrics" + assert_response :success + assert_includes response.body, "ScopedAgent" + ensure + ActiveAgent::Dashboard.trace_model_class = nil + Object.send(:remove_const, :ScopedTelemetryTrace) + end + test "engine root renders the traces index" do - get "/active_agent/" + get "/activeagents/" assert_response :success end test "dashboard overview redirects to traces in ERB mode" do - get "/active_agent/dashboard" + get "/activeagents/dashboard" assert_response :redirect - assert_includes response.location, "/active_agent/traces" + assert_includes response.location, "/activeagents/traces" end test "dashboard refuses unauthenticated access in production when no auth is configured" do Rails.env.stub(:production?, true) do - get "/active_agent/traces" + get "/activeagents/traces" end assert_response :forbidden assert_includes response.body, "authentication_method" end - test "local ingest endpoint matches LOCAL_ENDPOINT_PATH and persists traces" do - endpoint = ActiveAgent::Telemetry::Configuration::LOCAL_ENDPOINT_PATH - assert_equal "/active_agent/api/traces", endpoint + test "local endpoint path derives from the engine's actual mount" do + config = ActiveAgent::Telemetry::Configuration.new + config.local_storage = true + assert_equal "/activeagents/api/traces", config.local_endpoint_path + assert_equal "/activeagents/api/traces", config.resolved_endpoint + end + + test "local ingest endpoint persists traces" do payload = sample_payload - post endpoint, params: { traces: [ payload ], sdk: { name: "activeagent" } }, as: :json + post "/activeagents/api/traces", params: { traces: [ payload ], sdk: { name: "activeagent" } }, as: :json assert_response :accepted trace = ActiveAgent::TelemetryTrace.find_by(trace_id: payload["trace_id"]) @@ -81,6 +115,33 @@ def setup assert_equal 100, trace.total_input_tokens end + test "local ingest requires the configured ingest_api_key" do + ActiveAgent::Dashboard.ingest_api_key = "secret-ingest-key" + payload = sample_payload + + post "/activeagents/api/traces", params: { traces: [ payload ], sdk: {} }, as: :json + assert_response :unauthorized + + post "/activeagents/api/traces", params: { traces: [ payload ], sdk: {} }, as: :json, + headers: { "Authorization" => "Bearer wrong" } + assert_response :unauthorized + assert_nil ActiveAgent::TelemetryTrace.find_by(trace_id: payload["trace_id"]) + + post "/activeagents/api/traces", params: { traces: [ payload ], sdk: {} }, as: :json, + headers: { "Authorization" => "Bearer secret-ingest-key" } + assert_response :accepted + assert ActiveAgent::TelemetryTrace.find_by(trace_id: payload["trace_id"]) + ensure + ActiveAgent::Dashboard.ingest_api_key = nil + end + + test "exactly one install generator resolves" do + require "rails/generators" + generator = Rails::Generators.find_by_namespace("active_agent:dashboard:install") + + assert_equal ActiveAgent::Dashboard::InstallGenerator, generator + end + test "reporter local storage persists symbol-keyed tracer payloads" do config = ActiveAgent::Telemetry::Configuration.new config.enabled = true diff --git a/test/dummy/config/routes.rb b/test/dummy/config/routes.rb index f41192b4..5f81218c 100644 --- a/test/dummy/config/routes.rb +++ b/test/dummy/config/routes.rb @@ -2,7 +2,7 @@ # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Free self-hosted dashboard (exercised by test/dashboard tests) - mount ActiveAgent::Dashboard::Engine => "/active_agent" + mount ActiveAgent::Dashboard::Engine => "/activeagents" # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live.