Skip to main content

tracking

Server-side analytics tracking with pluggable providers. Use when adding analytics events, registering custom tracking providers, or configuring built-in providers (PostHog, Mixpanel, Amplitude, Webhook).

소스 정보

저장소
BuilderIO/agent-native
최근 소스 활동
2026년 10월 11일 17:11
감지된 SKILL.md 언어
영어
스타
7,119
포크
646

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
tracking
description
Server-side analytics tracking with pluggable providers. Use when adding analytics events, registering custom tracking providers, or configuring built-in providers (PostHog, Mixpanel, Amplitude, Webhook).
scope
dev
metadata
{"internal":true}
# Tracking ## Rule The tracking system provides a single server-side `track()` call that fans out to all registered providers, plus the browser-side `trackEvent()` counterpart. Built-in providers auto-register from env vars - set the var and tracking starts. Custom providers can be registered for any analytics backend. Both surfaces are best-effort and never block request handling. ## How It Works 1. At server startup, `registerBuiltinProviders()` checks env vars and registers any configured providers. 2. Application code calls `track(eventName, properties, source)` from actions, plugins, or server routes. 3. The registry fans out the event to every registered provider. Errors are caught and logged -- a failing provider never crashes the caller. 4. Built-in providers batch HTTP calls (flush every 10 seconds or 50 events, whichever comes first). ## API ### `track(name, properties?, source?)` Fire an analytics event. `source` is either a `{ userId, anonymousId, sessionId }` meta object or an action's `ctx` passed straight through. ```ts import { track } from "@agent-native/core/tracking"; // From an action — pass ctx; userId comes from ctx.userEmail. run: async ({ name }, ctx) => { track("meal.logged", { mealName: name, calories: 350 }, ctx); }; // From a plugin or route with no ctx. track( "meal.logged", { mealName: "Salad", calories: 350 }, { userId: "user@example.com" }, ); ``` The caller's browser session comes from the ambient request context (`RequestContext.browserSessionId`, set from the `X-Agent-Native-Session-Id` header), so it resolves the same whether the UI called the action or the agent did. Pass `sessionId` in the meta object to override it — routes that run outside a request context, such as `/_agent-native/track`, do exactly that. Providers map it to their own session field: `$session_id` for PostHog (which joins the event to session replay), `session_id` as a property for Mixpanel and Amplitude, a top-level `sessionId` for webhooks and Agent-Native Analytics. It is absent for callers with no browser — cron, CLI, MCP, A2A. ### `identify(userId, traits?)` Identify a user with traits. Forwarded to providers that support it. ```ts import { identify } from "@agent-native/core/tracking"; identify("user@example.com", { plan: "pro", company: "ExampleCo" }); ``` ### `registerTrackingProvider(provider)` Register a custom provider. ```ts import { registerTrackingProvider } from "@agent-native/core/tracking"; registerTrackingProvider({ name: "my-analytics", track(event) { // Send event to your backend }, identify(userId, traits) { // Optional }, flush() { // Optional -- called on graceful shutdown }, }); ``` ### `flushTracking()` Flush all providers (call before process exit). ## Built-in Providers Set the env var and the provider auto-registers at startup. No SDK dependencies -- all providers use raw HTTP. | Provider | Env vars | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | PostHog | `POSTHOG_API_KEY` (required), `POSTHOG_HOST` (optional, defaults to `https://us.i.posthog.com`), `POSTHOG_ERROR_TRACKING=false` (optional opt-out) | | Mixpanel | `MIXPANEL_TOKEN` | | Amplitude | `AMPLITUDE_API_KEY` | | Agent-Native Analytics | `AGENT_NATIVE_ANALYTICS_PUBLIC_KEY` (server), `AGENT_NATIVE_ANALYTICS_ENDPOINT` (optional, defaults to `https://analytics.agent-native.com/track`) | | Webhook | `TRACKING_WEBHOOK_URL` (required), `TRACKING_WEBHOOK_AUTH` (optional, sent as `Authorization` header) | Multiple providers can be active simultaneously. All receive every event. Browser-side `trackEvent()` also forwards to Agent-Native Analytics when `VITE_AGENT_NATIVE_ANALYTICS_PUBLIC_KEY` is present. Use `VITE_AGENT_NATIVE_ANALYTICS_ENDPOINT` to override the default browser endpoint. The built-in Agent-Native Analytics sender is quiet on localhost/local dev by default; set `AGENT_NATIVE_ANALYTICS_ALLOW_LOCALHOST=true` only for an intentional local ingestion test. An app served from a hosted beta host (`beta.*.agent-native.com`) reports to beta Analytics: `resolveLaneEndpoint` moves an endpoint on a production lane host to its beta lane for browser events, auth-page events, replay uploads, and server events. A new sender that reads the analytics endpoint must pass it through `resolveLaneEndpoint` too, or beta traffic splits. Beta and production Analytics share one database, so an ingest change runs on beta traffic first, and checking it on beta proves the merged code rather than production's. An endpoint on any other host is left as configured. ## Error Capture Exceptions fan out through `server/capture-error.ts` to every registered backend - Sentry, PostHog, and the tracking providers - from one `captureError()` call. Backends are additive: configuring a second one does not displace the first, and no backend is required for the others to work. When first-party Agent-Native Analytics is configured, browser `configureTracking()` captures uncaught errors, unhandled rejections, and manual `captureException()` calls as `$exception` events through `/track`. On the server, the core route plugin registers the tracking capture provider: `captureError()` calls `captureException()`, and the Agent-Native provider sends the same event when the server public key is configured. Analytics groups both into owner-scoped `error_issues` and `error_events`, shown in Monitoring -> Errors and exposed to authenticated agents through `list-error-issues` and `get-error-issue`. This remains available even when external Sentry is unavailable or rate-limited. Emit through `captureError()` / `captureException()`. Never hand-roll a `track("$exception", …)`: each backend needs its own payload shape and the providers build it. - **Provider-agnostic wiring must not live in a provider plugin.** The Nitro `error` hook is in `core-routes-plugin.ts`, not `sentry-plugin.ts`, because that plugin returns early when no `SENTRY_DSN` is set — hooking route errors there meant an app on any other backend silently reported none. - **`captureError()` is the one noise and flood boundary.** The drop rules live in `shared/error-noise.ts` (expected 4xx, access-control rejections, Lambda `socket hang up`, third-party/extension/GTM stacks, stale chunks, opaque `Script error.`); the Nitro hook, every explicit `captureError()` call, the browser capture, Sentry `beforeSend`, and the analytics ingest all call `classifyErrorNoise()`. After the filter, `server/capture-error.ts` folds transient-database, database-credential, and configuration failures into one aggregate per (class, route): the first event immediately, then one summary per window carrying `suppressedCount`. Every drop is counted (`getCaptureErrorStats()`); an error that matches no rule is reported every time. A backend must hang off `registerErrorCaptureProvider`, never call a provider SDK from a catch block, or it skips both. Two escape hatches keep a rule from hiding a real failure: tag a capture `reportExpected: "true"` (`REPORT_EXPECTED_FAILURE_TAG`) when your code raises an access-control or 4xx failure as a failure (a runner whose grant was lost), and give an explicit browser capture a `context` or `area` tag so a stackless Safari/Firefox network error from it is not dropped as unattributable. Stale-chunk failures are dropped because route-chunk-recovery reloads on them; when it cannot (cooldown, desktop) it reports one `RouteChunkRecoveryExhausted` per page session instead. - **A backend can accept a malformed payload and still show a count.** PostHog ingested the framework's camelCase `$exception` for a long time and rendered empty, ungroupable issues — which reads as coverage, not as breakage. When adding or changing a backend, check what an event looks like in its UI, not just that the request returned 200. - **Attribute the error.** Without a user id, server exceptions land under `anonymous` and split one person in two against their browser events. Pass `aiTraceId` for anything inside an agent run so the issue and the LLM trace resolve to each other. - **Every server capture carries a failure packet** at `extra.failureContext` (`observability/failure-context.ts`): app, route, action or automation name, `threadId`, `runId`, `requestId`, `userScope` (`org` or `personal`, never an email), `threadUrl` (`https://<app host>/?thread=<chat_threads.id>`), build, environment, `errorCode`, `failureClass`. The boundary derives it from what the call site already passes (`tags.action`, `extra.runId` / `threadId` / `request_id`, `aiTraceId`) and from the ambient chat run, so a capture inside a run names its thread with no change at the call site. Work that runs outside any request (a scheduled automation) passes `failure: { threadId, automationName, userScope }` on the capture. A run id also becomes `aiTraceId`. The Analytics issue page links `threadUrl`; hand any agent the packet and it can run `get-agent-thread-debug` with the `runId`. Never put an email, a token or a prompt in it. - **Rates are events, not log lines.** `tracking/failure-counters.ts` counts a failure class per (event, bounded dimensions) and ships `count` per window (first occurrence at once, the rest once a minute, `SUM(count)` is exact): `action_error_counts` (`action_name`, `error_code`, `status_class`, `action_source`) and `credential_state_counts` (`credential_state`, `credential_subject`, `source`). Automation pauses and automatic resumes are `automation_paused` / `automation_resumed` (`automation_hash`, never the name), the client's open circuits are `action_circuit_tripped`, and a chat run that ended on a credential problem is a `$ai_trace` with `credential_state`. Attachment mint/resolve/delete outcomes (ok included) are `attachment_outcome_counts` (`operation`, `status`, `reason`, `who_can_fix`), and a template counts its own classes with `countOutcome("<thing>_counts", { ...tokens })` from `@agent-native/core/tracking` (Mail's Gmail cooldowns are `gmail_cooldown_counts`, by `site`). Never one event per failure; a name that is not `<thing>_counts` is dropped with one console error. - **Browser outcomes are bounded, too.** `agent_run_outcome` is one event per run (`outcome`, legacy `code`, `terminal_source`, `verified_after_pipe_closed`, `resume_attempts`, `run_id`, `thread_id`): every `interrupted` / `failed` / `unverified` run up to 30 per page, every `stopped` run up to its own 30 (so stops never crowd out failures), and `succeeded` sampled at 10% with `sample_weight`. A turn the server refuses at its start (no model connected, a 5xx) is a `failed` run too, with `terminal_source: local` and the refused turn's id. A failed or interrupted run adds its `cause` from `AGENT_TROUBLE_CAUSES` when one fits its `code`, and Analytics groups the rest by `code`. A code that is not an identifier is sent as `unrecognized_code` (`agentErrorCodeForTelemetry`), because a thrower's `data.code` can be a sentence. It never carries the run's message, not even redacted: no pattern can tell an unquoted document or person's name from the words around it, so a message stays out of every event. A provider error is `provider_error` only when its code came from a structured HTTP status (`http_5xx`, including a status on a wrapped cause); a status that appears only in message text is not read. `agent_feedback_submitted` (`sentiment`, `run_id`, `thread_id`) is the browser's copy of a thumbs rating, because `$ai_feedback` has no browser session. Every `pageview` carries `agent_signals: 1` (`AGENT_SIGNALS_PAGEVIEW_PROPERTY`): clients before it sampled stops at 10% and sent no ratings or `page_load_id`, so Analytics reads a session's cancelled runs, thumbs-down, and quick backs as measured only after seeing the marker and while no unmarked pageview or sampled stop shares the session (tabs share one session id), and counts only stops sent unsampled. Keep all three guarantees while the marker ships. Every `pageview` also carries `page_load_id` (`PAGE_LOAD_PAGEVIEW_PROPERTY`), the same until the page reloads, because quick backs compare pages only within one page load. `session_navigation` is one event per document that left because of the session (`reason`, and for `signed_out` the `evidence`: `signed_out_body` or `http_401`), never the destination. Both are emitted from the single place that decides (`agentkit-protocol.ts`, `navigateForSession`; a refused start from the transport's start-run catch), not the callers. `agent_chat_stuck_detected` fires once per run and only while the stuck banner shows, so Analytics' stuck chats are ones the person saw. It carries `reason`, `dispatchMode`, `hasInFlightWork` and `heartbeatSinceSec`, so a run the server still holds (a live background worker) reads differently from a dead one. Symbolication is per-backend and not automatic: the framework uploads no source maps to PostHog, so minified browser stacks stay minified there. Known gap, not a bug to re-diagnose. ### Browser keys and the SSR shell Public keys (`POSTHOG_PUBLIC_KEY`, the Sentry client DSN, and the first-party Analytics public key) ship inside the CDN-cached SSR shell — publishable and identical for every visitor. Server keys never do, and are never a fallback for a public one: `POSTHOG_API_KEY` may be a private key and this value lands in public HTML. Browser errors post directly to the backend rather than through `/_agent-native/track`, because that route requires a resolved session and relaying would drop every signed-out crash. When adding a client config field, update **both** `server/posthog-config.ts` and the mirrored worker emitter in `deploy/build.ts` — the worker bundles a string copy and cannot import the module, so a one-sided edit drops the config silently in deployed builds. `posthog-config.spec.ts` pins the two outputs together. ## MCP Server Events The MCP server an app exposes reports its own usage. `packages/core/src/mcp/analytics.ts` emits one event per protocol request, and the emission points sit in the shared server builder (`build-server.ts`), so the HTTP mount and the stdio transport report identically. | Event | Fires on | | ---------------------- | --------------------------------------------- | | `$mcp_initialize` | the client/server handshake (HTTP mount only) | | `$mcp_tools_list` | `tools/list` | | `$mcp_tool_call` | `tools/call`, success or failure | | `$mcp_resources_list` | `resources/list` | | `$mcp_resource_read` | `resources/read` | Names come from PostHog's MCP analytics vocabulary (https://posthog.com/docs/mcp-analytics/events) on purpose, so PostHog's MCP dashboards read these events with no mapping layer — but they go through `track()` like every other event, so Mixpanel, Amplitude, a webhook, and Agent-Native Analytics receive the same ones. Shared properties: `$mcp_source` (`http` / `stdio`), `$mcp_server_name`, `$mcp_server_version`, `$mcp_app_id`, `$mcp_client_name`, `$mcp_client_version`, `$mcp_client_user_agent`, `$mcp_vendor_client`, `$mcp_protocol_version`. Per event: `$mcp_tool_name`, `$mcp_tool_description`, `$mcp_tool_category`
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기