tracking
Skill7.6k starsChanged 5 months agoMITView on GitHub
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).
- Reads credentials
We split this skill into the rules it gives an agent
7 rules. About 0 are common, 1 is distinctive, and 6 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 122
When first-party Agent-Native Analytics is configured, browser configureTracking() captures uncaught errors, unhandled rejections, and …
- Very specific: line 256
When adding a client config field, update both server/posthog-config.ts and the mirrored worker emitter in deploy/build.ts — the worker …
- Very specific: line 270
Every third-party browser tag goes through the gate: server-injected scripts use consentGatedScript() in server/analytics.ts.
- Very specific: line 291
Use consentDecision(category) for anything a revoke can't undo once sent.
- Distinctive: line 372
Does not send first-party events from localhost/local dev
- Very specific: line 376
Every browser-side trackEvent() POST to the Agent-Native Analytics /track endpoint includes:
- Very specific: line 493
All new event names and properties use lowercase snake_case.
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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
- At server startup,
registerBuiltinProviders()checks env vars and registers any configured providers. - Application code calls
track(eventName, properties, source)from actions, plugins, or server routes. - The registry fans out the event to every registered provider. Errors are caught and logged -- a failing provider never crashes the caller.
- Built-in providers batch HTTP calls (flush every 10 seconds or 50 events, whichever comes first). Each send times out after 10 seconds.
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.
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.
import { identify } from "@agent-native/core/tracking";
identify("user@example.com", { plan: "pro", company: "ExampleCo" });registerTrackingProvider(provider)
Register a custom provider.
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
errorhook is incore-routes-plugin.ts, notsentry-plugin.ts, because that plugin returns early when noSENTRY_DSNis 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 inshared/error-noise.ts(expected 4xx, access-control rejections, Lambdasocket hang up, third-party/extension/GTM stacks, stale chunks, opaqueScript error.); the Nitro hook, every explicitcaptureError()call, the browser capture, SentrybeforeSend, and the analytics ingest all callclassifyErrorNoise(). After the filter,server/capture-error.tsfolds transient-database, database-credential, and configuration failures into one aggregate per (class, route): the first event immediately, then one summary per window carryingsuppressedCount. Every drop is counted (getCaptureErrorStats()); an error that matches no rule is reported every time. A backend must hang offregisterErrorCaptureProvider, 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 capturereportExpected: "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 acontextorareatag 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 oneRouteChunkRecoveryExhaustedper page session instead.- A backend can accept a malformed payload and still show a count. PostHog
ingested the framework's camelCase
$exceptionfor 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
anonymousand split one person in two against their browser events. PassaiTraceIdfor 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(orgorpersonal, 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) passesfailure: { threadId, automationName, userScope }on the capture. A run id also becomesaiTraceId. The Analytics issue page linksthreadUrl; hand any agent the packet and it can runget-agent-thread-debugwith therunId. Never put an email, a token or a prompt in it. - Rates are events, not log lines.
tracking/failure-counters.tscounts a failure class per (event, bounded dimensions) and shipscountper 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) andcredential_state_counts(credential_state,credential_subject,source). Automation pauses and automatic resumes areautomation_paused/automation_resumed(automation_hash, never the name), the client's open circuits areaction_circuit_tripped, and a chat run that ended on a credential problem is a$ai_tracewithcredential_state. Attachment mint/resolve/delete outcomes (ok included) areattachment_outcome_counts(operation,status,reason,who_can_fix), and a template counts its own classes withcountOutcome("<thing>_counts", { ...tokens })from@agent-native/core/tracking(Mail's Gmail cooldowns aregmail_cooldown_counts, bysite). Never one event per failure; a name that is not<thing>_countsis dropped with one console error. - Browser outcomes are bounded, too.
agent_run_outcomeis one event per run (outcome, legacycode,terminal_source,verified_after_pipe_closed,resume_attempts,run_id,thread_id, andturn_idwhen the page started the turn, except a replay whose turn the server did not name): everyinterrupted/failed/unverifiedrun up to 30 per page, everystoppedrun up to its own 30 (so stops never crowd out failures), andsucceededsampled at 10% withsample_weight. A turn the server refuses at its start (no model connected, a 5xx) is afailedrun too, withterminal_source: localand the refused turn's id. A failed or interrupted run adds itscausefromAGENT_TROUBLE_CAUSESwhen one fits itscode, and Analytics groups the rest bycode. A code that is not an identifier is sent asunrecognized_code(agentErrorCodeForTelemetry), because a thrower'sdata.codecan 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 isprovider_erroronly 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_feedbackhas no browser session. Everypageviewcarriesagent_signals: 1(AGENT_SIGNALS_PAGEVIEW_PROPERTY): clients before it sampled stops at 10% and sent no ratings orpage_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. Everypageviewalso carriespage_load_id(PAGE_LOAD_PAGEVIEW_PROPERTY), the same until the page reloads, because quick backs compare pages only within one page load.session_navigationis one event per document that left because of the session (reason, and forsigned_outtheevidence:signed_out_bodyorhttp_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_detectedfires once per run and only while the stuck banner shows, so Analytics' stuck chats are ones the person saw. It carriesreason,dispatchMode,hasInFlightWorkandheartbeatSinceSec, 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.
Cookie consent
Visitors in the consent region (shared/consent.ts, copied from builder.io's
banner rules) see ConsentBanner, which AppProviders and the docs root
mount, and choose two categories: performance (Google Analytics,
Amplitude, Ahrefs) and targeting (Google Tag Manager, attribution).
Everyone else gets no banner and every tag at once.
- Every third-party browser tag goes through the gate: server-injected
scripts use
consentGatedScript()inserver/analytics.ts; client code useswhenConsentAllows(category, run)from@agent-native/core/client/consent. A tag in a raw<script>or a<noscript>fallback runs before the visitor chooses. - The GTM container (
GTM_CONTAINER_ID, shared by every app and the docs site) carries the ad pixels: Meta, LinkedIn, Reddit, X, Bing UET, Google Ads and OpenAI, plus HubSpot, Dreamdata, Vector, Unify, Common Room and Apollo. Gating the container gates them all, which is why it sits undertargeting. Tags added inside GTM need no code change; tags added anywhere else do. - The inline bootstrap in
server/analytics.tsruns the queue on pages that never load the app bundle. It repeats the cookie format and time-zone rules ofclient/consent.ts; change both together. Each queued loader runs once a page, even after a reject and a fresh accept. - A loaded tag can't be unloaded, so Google's tags follow Consent Mode: the
server pushes a
consent defaultfrom the stored choice (all denied without one) to every Google data layer before any tag, andclient/consent.tspushes aconsent updateon each region answer and choice. Our owngtagevents also wait forperformance. Non-Google tags in GTM stop only on the next page load. - Use
consentDecision(category)for anything a revoke can't undo once sent; it isnulluntil the visitor's choice or the server's region is known.consentAllowsNowguesses from the time zone, so only pair it with storage arevokecan clear. - First-party analytics keeps counting in the region before consent, but the
visitor id
an_aidwaits forperformance: until thengetOrCreateAnalyticsAnonymousId()returnsundefinedand stores nothing, and events go out with noanonymousIdandproperties.analytics_consent: "withheld". Events sent before the region is known wait for it, or leave without the id, markedanalytics_consent: "unknown", if the page closes first. The plain sign-in page reads the id through the same helper and holds its events withwhenConsentDecided; never writean_aiddirectly. A choice made in another tab applies at once in tabs of the same app, and on focus in another app on the site. A reject clears a stored id. Those rows count as events and views but never as people; the Analytics activation funnel joins them to a signup in the same session. Session replay waits forperformancetoo, with no time-zone head start; a reject stops it, discards what it hasn't uploaded (including a batch an upload in flight puts back), and forgets its stored replay identity. Amplitude rechecks the choice before it initializes and never sends events from before an accept. Google events held for the region's answer go out only if it allows them. Sentry error capture and the session idan_sidaren't gated.
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
(read / write), $mcp_listed_tool_names, $mcp_duration_ms,
$mcp_is_error, $mcp_error_type, $mcp_error_message, $mcp_resource_name,
$mcp_resource_uri.
- A client's own name is only on the wire at
initialize. The mount is stateless — one server per request — so no later event can recover it.$mcp_initializecarries theclientInfofrom the handshake; every other event falls back to the 2026-era per-request_metaand the HTTP user agent, and$mcp_vendor_clientbuckets both spellings onto one row. - A failed call is reported with its reason, not just
isError. Thetools/callhandler renders "unknown tool", "forbidden scope", and a thrown action error as the same shape of error result, so each sets$mcp_error_typewhere it returns rather than having it guessed back out of the response text. - Payloads stay out.
$mcp_responseis never emitted.$mcp_parametersis off by default and redacted when on — tool arguments carry user content.
Off switches: MCP_ANALYTICS=false (observability.mcpEvents) disables the
events; MCP_ANALYTICS_PARAMETERS=true (observability.mcpCaptureParameters)
opts into arguments. They sit in observability with the other capture
switches, not in analytics — they gate every provider, not the first-party
Agent-Native Analytics sender whose key lives there.
Default Baseline Events
Template roots call configureTracking() once during app startup. That installs default browser pageview tracking for hosted apps:
- Event:
pageview - Fires on initial load,
history.pushState,history.replaceState, andpopstate - De-dupes repeated events for the same URL
- Includes
url,path,hostname,referrer,title,navigation_type,agent_signals,page_load_id,app, and inferredtemplate - Includes LLM connection context on browser events when known:
llm_connection(builder,anthropic,openai, etc.),llm_engine,llm_model,llm_connection_source, andllm_connection_configured - Does not send first-party events from localhost/local dev
Visitor identity (anonymousId + sessionId)
Every browser-side trackEvent() POST to the Agent-Native Analytics /track endpoint includes:
anonymousId— persistent per-browser visitor ID stored inlocalStorageunderagent-native.anonymous_id. Generated once and reused across sessions. Use this for unique-visitor and returning-visitor metrics. In the consent region it's absent until the visitor accepts analytics cookies (see Cookie consent).sessionId— rotating per-visit ID stored inlocalStorageunderagent-native.session_id, with a 30-minute idle timeout (matches GA4 / Mixpanel defaults). Use this for sessions-per-visitor, pages-per-session, and session-duration metrics.userId— only set when the calling code passesproperties.userId. Anonymous traffic leaves this NULL by design;anonymousIdis the fallback.
These fields land in the analytics_events.anonymous_id, analytics_events.session_id, and analytics_events.user_id columns in the analytics template. Storage access is wrapped in try/catch — private-browsing / blocked-storage clients silently degrade to NULL rather than crashing the page.
Referral / viral attribution (first-touch)
configureTracking() also captures an anonymous visitor's first-touch referral context once, on first page load, and persists it across the signup boundary so the server-side signup event records where the user came from. This powers virality metrics for every template (Clips share links, Plans public pages, etc.).
There is one capture implementation, captureAttribution() from @agent-native/core/client/analytics. Every page that can be someone's first visit calls it, including the server-rendered sign-in page, which never runs configureTracking() and passes { landingPath } so landing_path names the app path behind the sign-in page. Never copy the capture into a page: the sign-in page's private copy silently dropped ad click ids, site handoff fields, and last touch for every visitor who landed on it.
Share-link params (set by whatever generates the link; read client-side only):
ref— referral source bucket, e.g.clip_share,plan_sharevia— the referrer's stable user id (the clip/plan owner)utm_source,utm_medium,utm_campaign,utm_content,utm_term
Client persistence (first-write-wins, except that the first visit with a source replaces one without; last touch lives in an_last_touch / an_lt):
localStoragekeyan_attributionand first-party cookiean_ft(path=/; max-age=2592000; SameSite=Lax, not HttpOnly — non-sensitive, written by client JS).- Both store the same URL-encoded compact JSON (empty fields omitted, each value capped at 120 chars):
{ ref, via, utm_source, utm_medium, utm_campaign, utm_content, utm_term, landing_path, landing_referrer, landed_at }.landing_referreris the host only ofdocument.referrer(scrubbed; same-origin referrers are dropped). - A durable copy, cookie
an_fts, set only by our server (POST /_agent-native/auth/attribution, same-origin, no session needed) and expiring 365 days afterlanded_at, never extended by later visits. The browser sends its first touch there each load until the server holds that visit. Safari caps cookies written by page JavaScript at seven days, so only a server-set cookie can keep first touch for a year. The value carriesv: "1"; a reader ignores other versions. When the browser's copies are gone, capture restores them froman_fts. Each sync after the browser writesan_ftoran_ltalso has the server write them again from the request's cookies, so Safari keeps them their full 30 days too. - Every copy is compared with
preferFirstTouch(shared/first-touch-persistence.ts): the first visit with a source, else the first visit. Never add a second ordering rule: the server cookie route, the browser, and signup must agree on which visit is first touch, or a slow response from one tab overwrites a better touch from another. - Attribution is a
targetingconsent category. Visitors in the consent region (builder.io's GDPR list: the EU, the rest of the EEA, Monaco, San Marino, Åland and France's overseas regions; not the UK or Switzerland, inpackages/core/src/shared/consent.ts) get nothing stored until they opt in through thean_consentcookie. The banner writesan_consentin the page and thenPOST /_agent-native/auth/consentwrites it again from the server, so Safari keeps the choice its full year. The browser gates capture withwhenConsentAllows("targeting", …)fromclient/consent.ts, which asksGET /_agent-native/auth/consent-region(the request's country) and falls back to the time zone; a reject clears every copy. The server refusesan_ftsand clears all three cookies by request country unlessan_consentgrants targeting. Everyone else is stored as before, with no banner. Don't add attribution storage that bypassescaptureAttribution(), or it skips this check. getFirstTouchAttribution()(from@agent-native/core/client) returns the parsed object ornull.
Signup event enrichment (server-side, from the an_fts and an_ft cookies on the signup/OAuth-callback request, whichever preferFirstTouch picks, with an_fts winning a tie, derived in packages/core/src/server/attribution.ts):
referral_source—refif present, else derived:/share/…→clip_share; a plan public path (/p/,/plan/,/share-plan/) →plan_share; a non-empty external referring host →external; otherwisedirect.referrer_user(=via),referral_medium(=utm_medium),referral_campaign(=utm_campaign)utm_source,utm_medium,utm_campaign,utm_content,utm_term(raw passthrough)first_touch_path(=landing_path),landing_referrer
Attribution parsing is fully defensive and never blocks signup — a missing/malformed cookie falls back to referral_source: "direct".
Other framework-level baseline events:
session statusfromuseSession(), withsigned_inaction.responsefrom the browser action transport, with action name, theroutetemplate of the page that made the request (omitted when no manifest route matches), browser-perceived duration and TTFB, response status/outcome, response size when known, and parsedServer-Timingphases for framework readiness and database work. Itsrequest_idjoins the exact browser and server events. This separates server time from CDN/network/body overhead. One at or overSLOW_ACTION_RESPONSE_MS(1 s) is never sampled. When someone waited for it (isWaitedActionResponse: the page stayed visible and the request was not cancelled), it is also marked on the session replay (agent-native.slow_request) with its action, method, duration, status, outcome, andpage_hidden.web_vitalsonce per page view, with the React Routerroutetemplate (/sessions/:id, never the ids; omitted when no manifest route matches, because a normalized raw path still carries slugs and emails. It is the one tracked event with nourlorpath, even fromgetDefaultProps:ROUTE_ONLY_EVENT_NAMESinclient/analytics.tsstrips them),navigation_type(load,client, orresumeafter the tab was hidden, sent only when it saw an interaction or layout shift), andttfb_ms,lcp_ms,inp_ms, andcls. TTFB and LCP exist only for document loads, a load in a background tab is not reported, and a metric the browser cannot measure (including CLS when the layout-shift observer fails) is omitted rather than sent as 0. A page view ends onpushState/popstateto another path or when the tab is hidden;replaceStatekeeps it, so redirects land on the final route. Measured with nativePerformanceObservers;configureTracking({ webVitals: false })turns it off. Each page view also marks the replay.- Request timing is an OpenTelemetry
http.serverspan, not a product event. Each framework HTTP request records method, bounded route template, status code, first-request-in-isolate cold marker, framework readiness wait, and database operation count and wall time. The span goes only to the registered OpenTelemetry provider and never reaches analytics providers, so it cannot fire product alerts. Its status iserrorfor 5xx responses only. Ingest endpoints,/api/analytics/replay, andAGENT_NATIVE_HTTP_TELEMETRY_DISABLEDrecord no span. Cold or 1 s-plus requests also write anagent-native.slow_requestlog line with process age, readiness wait, and database timings. Responses carryServer-Timingforapp,startup,db,db-connect, anddb-slowest(plusstartup-db-*for initialization database work) and anX-Agent-Native-Request-Idheader where applicable. Query text and parameters are never captured. There is no sampling knob for this span; export volume is governed by the OpenTelemetry sampler. signupfrom Better Auth user creation, withauth_provider,auth_user_id, and first-touch referral attribution (referral_source,referrer_user,referral_medium,referral_campaign,utm_*,first_touch_path,landing_referrer— see "Referral / viral attribution" above)builder connect clickedandbuilder connect popup blockedfrom browser Use Builder.io CTAsbuilder connect started,builder connect succeeded,builder connect failed,builder disconnect succeeded, andbuilder disconnect failedfrom the Builder connection routes, with LLM connection context when resolvable$ai_generationfrom instrumented agent loops, with PostHog AI Observability fields such as$ai_trace_id,$ai_session_id,$ai_model,$ai_provider,$ai_input_tokens,$ai_output_tokens,$ai_latency,$ai_total_cost_usd, and mirrored Agent-Native query fields such asrun_id,thread_id,turn_id,cost_cents_x100,duration_ms,tool_calls, andstatus. A boundedtoolsarray contains names, start offsets, durations, statuses, and coarse error classes only; interrupted tools and failed runs remain visible, and delegated runs include protocol/task/parent-run/parent-turn correlation. Run failure messages are omitted from every tracking provider even with content capture enabled.$ai_errorcarries a fixed message derived fromterminal_code, a namedcause, terminal state, and retryability. Theerror_messagefield and tool error output text are absent. Local trace SQL retains debugging detail under the observability capture settings.agent_run_terminalis one event per run: from the run's own process, or from the SQL write that ended it for that process (a reap withreaped: true,reap_source,recovery_outcome; a stop withstatus: "aborted"andabort_reason; the reaper's repair from the event log; a run that fails or is released before its worker starts). Not sent yet: a background chunk that hands off to a continuation, and a run the production agent marks errored before its run loop starts (a preparation, history or dispatch failure). A row aborted asduplicate_submissionorexecution-lease-busystood in for another run and sends none. An event sent from a SQL write has nomodel,engineorattempt_count, since only the run's process knows them; every reported aborted run is sent this way. It carriesturn_idand the run's user (the turn's initiator when a SQL write ended it, never the request that ended it; none for an anonymous visitor).run_startedalso carriesturn_id.agent_turn_submittedis one browser event per agent request the server accepts (not on send, so a busy-slot refusal then queue counts once; a replay is not counted, including a retry the server answers with the run it already started), with theturn_idthe server stores on the run,initiator(user,appforsendToAgentChatincluding a send deferred while the thread restored,systemfor continuations, recoveries, approvals, setup replays and background sends),surface, andsubmit_message_id/recovery_of_run_idwhen present. Continuations repeat the turn'sturn_idassystem, so count turns by distinctturn_id. A send routed to Builder or an MCP host is reported once withrouted_toand noturn_id, also when the host has not confirmed it. A queued message the server's inbox drain admits sends noagent_turn_submittedyet; its run still reportsagent_run_terminalwithturn_idqueued-<message id>. A queued message the browser promotes is counted.agent_run_terminalcarrieserror_codeanderror_cause, nevererror_detail. Run and completion captures, and Builder gateway failures, usecaptureError(..., { errorMessagePolicy: "omit" }): providers receive the original exception type, stack frames without the original message header or nested causes, and a fixedInternal Server Errormessage. Noise filtering still sees the original failure; flood summaries use the fixed label. Monitoring groups by app, type, frame, error code, and failure class instead of user-specific message wording.
For new lifecycle events, call track() server-side when the server is the source of truth, and trackEvent() client-side only for browser interactions.
Lifecycle taxonomy
Lifecycle event names use lowercase snake_case; lifecycle properties use the
same convention. The shared dimensions are app_name, template_name,
user_id, user_email, workspace_id, session_id, output_id,
output_type, source, and referrer. App/template values are canonical ids
without the agent-native- prefix. Identity is also stored in the event's
top-level user/session columns for joins.
The canonical lifecycle events are app_entered, core_action_started,
core_action_completed, core_action_failed, output_viewed,
output_shared, cta_clicked, return_usage, and cross_app_used. The
framework also emits action_started, action_completed, and
action_failed for mutating defineAction() calls so UI, agent, MCP, A2A,
automation, and CLI activity share one action-level trail. Read-only actions
and high-frequency refresh, polling, navigation, and application-state actions
are excluded.
App entry is emitted once per app and browser session. Tracking is action-level only: it does not install mouse, pointer, scroll, keypress, or DOM autocapture. Existing legacy events remain available and emit their canonical lifecycle counterpart where the meaning is unambiguous, so downstream dashboards can migrate without losing historical names.
Event name migration
All new event names and properties use lowercase snake_case. The shared
track() and browser trackEvent() emitters preserve a legacy event exactly
and, for names in LEGACY_TRACKING_EVENT_NAME_ALIASES, emit a canonical alias
with legacy_event_name and canonical_event_name provenance properties. Use
the canonical alias for new dashboards and new call sites; do not normalize
historical warehouse rows or add new legacy names. Provider/framework names
such as $ai_*, $mcp_*, $exception, action.response, and
app.first_action are intentional exceptions.
Provider Interface
interface TrackingProvider {
name: string;
track(event: TrackingEvent): void | Promise<void>;
identify?(
userId: string,
traits?: Record<string, unknown>,
): void | Promise<void>;
flush?(): void | Promise<void>;
}
interface TrackingEvent {
name: string;
properties?: Record<string, unknown>;
timestamp?: string;
userId?: string;
}Design Decisions
- globalThis singleton -- the registry uses a
Symbol.forkey on globalThis so multiple ESM graph instances (dev-mode Vite + Nitro, symlinks) share one provider set. - Best-effort fan-out -- provider errors are caught and logged, never propagated. A broken analytics integration must not break app functionality.
- Batched HTTP -- built-in providers enqueue events and flush every 10 seconds or 50 events, minimizing outbound requests.
- NOT bridged to the event bus -- tracking and the event bus are separate concerns. The event bus is for triggering automations; tracking is for analytics. Do not subscribe to
track()calls from the event bus or vice versa.
Key Files
| File | Purpose |
packages/core/src/tracking/registry.ts | track(), identify(), registerTrackingProvider(), flushTracking() |
packages/core/src/tracking/providers.ts | Built-in providers (PostHog, Mixpanel, Amplitude, Agent-Native Analytics, Webhook) and registerBuiltinProviders() |
packages/core/src/tracking/types.ts | TrackingEvent and TrackingProvider interfaces |
packages/core/src/tracking/posthog-exception.ts | $exception_list builder + stack-frame parser (isomorphic: server and browser) |
packages/core/src/tracking/redaction.ts | Shared bounding/redaction helpers used by every exception emitter |
packages/core/src/shared/error-noise.ts | The one noise rule set (classifyErrorNoise) plus frame-origin and PII scrubbers; browser, server, Sentry, and ingest all call it |
packages/core/src/server/error-noise-filter.ts | Provider-agnostic drop rules, applied by both Sentry beforeSend and the route error hook |
packages/core/src/server/posthog-config.ts | Public browser PostHog config (mirrored in deploy/build.ts) |
Related Skills
secrets-- API keys for tracking providers can be registered as secretsserver-plugins--registerBuiltinProviders()is called by the core-routes plugin at startupactions-- calltrack()from action handlers to record user/agent activity
Trending
More talk lately.about the whole repository
Who's talking about BuilderIO/agent-native
- @github-trending-js.bsky.social“📦 BuilderIO / agent-native ⭐ 4,978 (+89) 🗒 TypeScript A framework for building agentic apps”Bluesky · Sep 20, 2026 · 2 likes
- @github-trending-js.bsky.social“📦 BuilderIO / agent-native ⭐ 4,011 (+96) 🗒 TypeScript A framework for building agent-native applications.”Bluesky · Jul 26, 2026 · 2 likes
- @epicvibecoder.bsky.social“agent-native: a TypeScript framework from Builder.io for apps where AI agents are first-class users, not an …”Bluesky · Jun 20, 2026 · 2 likes
- @github-trending-js.bsky.social“🔥 Hot Repo! 🔥 (100+ new stars) 📦 BuilderIO / agent-native ⭐ 7,378 (+107) 🗒 TypeScript A framework for …”Bluesky · Oct 16, 2026 · 1 likes
- @dailygithubtrends.bsky.social“今日のGitHubトレンド BuilderIO/agent-native Agent-Nativeは、自律的に動作するエージェントと専用UIを組み合わせて開発するためのTypeScriptフレームワークです。…”Bluesky · Sep 23, 2026 · 1 likes
The biggest 5 of 61 mentions linking to the repository that our crawl found. Each line is their own headline.
See all 61
Talked about on GitHub and Bluesky (61 times).
On GitHub · 50
- Oct 28, 2026agent-skills.github.builderio.agent-native: 0-unstable-2026-10-27 → 0.1.287 — sudosubin/agents.nix0 reactions
- Oct 28, 2026claude-code-marketplaces.github.builderio.agent-native: 0-unstable-2026-10-27 → 0.1.287 — sudosubin/agents.nix0 reactions
- Oct 28, 2026codex-marketplaces.github.builderio.agent-native: 0-unstable-2026-10-27 → 0.1.287 — sudosubin/agents.nix0 reactions
- Oct 28, 2026codex-plugins.github.builderio.agent-native: 0-unstable-2026-10-27 → 0.1.287 — sudosubin/agents.nix0 reactions
- Oct 28, 2026agent-plugins.github.builderio.agent-native: 0-unstable-2026-10-27 → 0.1.287 — sudosubin/agents.nix0 reactions
45 more on GitHub
- Oct 28, 2026claude-code-plugins.github.builderio.agent-native: 0-unstable-2026-10-26 → 0.1.287 — sudosubin/agents.nix0 reactions
- Oct 27, 2026agent-skills.github.builderio.agent-native: 1a39024 → efdb9df — sudosubin/agents.nix0 reactions
- Oct 27, 2026claude-code-marketplaces.github.builderio.agent-native: 869063e → 06e5709 — sudosubin/agents.nix0 reactions
- Oct 27, 2026codex-marketplaces.github.builderio.agent-native: ba4ef37 → 06e5709 — sudosubin/agents.nix0 reactions
- Oct 27, 2026codex-plugins.github.builderio.agent-native: 55d65b3 → 0cc74c0 — sudosubin/agents.nix0 reactions
- Oct 27, 2026chore: update Agent Native exported skills — BuilderIO/skills0 reactions · 1 comment
- Oct 27, 2026agent-plugins.github.builderio.agent-native: 1fcca73 → cc341a4 — sudosubin/agents.nix0 reactions
- Oct 27, 2026agent-plugins.github.builderio.agent-native: 36006e6 → 1fcca73 — sudosubin/agents.nix0 reactions
- Oct 27, 2026agent-skills.github.builderio.agent-native: 3e6443d → 1a39024 — sudosubin/agents.nix0 reactions
- Oct 27, 2026claude-code-marketplaces.github.builderio.agent-native: 87ba124 → 869063e — sudosubin/agents.nix0 reactions
- Oct 27, 2026codex-marketplaces.github.builderio.agent-native: 12a91c6 → ba4ef37 — sudosubin/agents.nix0 reactions
- Oct 27, 2026codex-plugins.github.builderio.agent-native: 7af9ac7 → 55d65b3 — sudosubin/agents.nix0 reactions
- Oct 27, 2026agent-plugins.github.builderio.agent-native: e9d5ffa → 36006e6 — sudosubin/agents.nix0 reactions
- Oct 27, 2026agent-skills.github.builderio.agent-native: 0-unstable-2026-10-26 → 0-unstable-2026-10-27 — sudosubin/agents.nix0 reactions
- Oct 27, 2026claude-code-marketplaces.github.builderio.agent-native: eaa780a → 87ba124 — sudosubin/agents.nix0 reactions
- Oct 27, 2026codex-marketplaces.github.builderio.agent-native: 6b65dba → 12a91c6 — sudosubin/agents.nix0 reactions
- Oct 27, 2026claude-code-marketplaces.github.builderio.agent-native: 0-unstable-2026-10-26 → 0-unstable-2026-10-27 — sudosubin/agents.nix0 reactions
- Oct 27, 2026codex-marketplaces.github.builderio.agent-native: 0-unstable-2026-10-26 → 0-unstable-2026-10-27 — sudosubin/agents.nix0 reactions
- Oct 27, 2026codex-plugins.github.builderio.agent-native: 0-unstable-2026-10-26 → 0-unstable-2026-10-27 — sudosubin/agents.nix0 reactions
- Oct 27, 2026agent-plugins.github.builderio.agent-native: 0-unstable-2026-10-26 → 0-unstable-2026-10-27 — sudosubin/agents.nix0 reactions
- Oct 27, 2026agent-skills.github.builderio.agent-native: 7faf806 → 7397901 — sudosubin/agents.nix0 reactions
- Oct 26, 2026claude-code-marketplaces.github.builderio.agent-native: e0ebd6b → 5252c0e — sudosubin/agents.nix0 reactions
- Oct 26, 2026codex-marketplaces.github.builderio.agent-native: e0ebd6b → 5252c0e — sudosubin/agents.nix0 reactions
- Oct 26, 2026codex-plugins.github.builderio.agent-native: e0ebd6b → 5252c0e — sudosubin/agents.nix0 reactions
- Oct 26, 2026agent-plugins.github.builderio.agent-native: 0ab7e59 → 5dc9ffe — sudosubin/agents.nix0 reactions
- Oct 26, 2026claude-code-plugins.github.builderio.agent-native: 0-unstable-2026-10-22 → 0-unstable-2026-10-26 — sudosubin/agents.nix0 reactions
- Oct 26, 2026agent-skills.github.builderio.agent-native: d2739c6 → 7faf806 — sudosubin/agents.nix0 reactions
- Oct 26, 2026chore: update Agent Native exported skills — BuilderIO/skills0 reactions · 1 comment
- Oct 26, 2026claude-code-marketplaces.github.builderio.agent-native: eb073a4 → e0ebd6b — sudosubin/agents.nix0 reactions
- Oct 26, 2026codex-marketplaces.github.builderio.agent-native: eb073a4 → e0ebd6b — sudosubin/agents.nix0 reactions
- Oct 26, 2026codex-plugins.github.builderio.agent-native: eb073a4 → e0ebd6b — sudosubin/agents.nix0 reactions
- Oct 26, 2026agent-plugins.github.builderio.agent-native: 0-unstable-2026-10-25 → 0-unstable-2026-10-26 — sudosubin/agents.nix0 reactions
- Oct 26, 2026agent-skills.github.builderio.agent-native: 0-unstable-2026-10-25 → 0-unstable-2026-10-26 — sudosubin/agents.nix0 reactions
- Oct 26, 2026claude-code-marketplaces.github.builderio.agent-native: 0-unstable-2026-10-25 → 0-unstable-2026-10-26 — sudosubin/agents.nix0 reactions
- Oct 26, 2026codex-marketplaces.github.builderio.agent-native: 0-unstable-2026-10-25 → 0-unstable-2026-10-26 — sudosubin/agents.nix0 reactions
- Oct 26, 2026codex-plugins.github.builderio.agent-native: 0-unstable-2026-10-25 → 0-unstable-2026-10-26 — sudosubin/agents.nix0 reactions
- Oct 26, 2026agent-skills.github.builderio.agent-native: 1fe5d77 → 8505239 — sudosubin/agents.nix0 reactions
- Oct 26, 2026claude-code-marketplaces.github.builderio.agent-native: 1fe5d77 → 8505239 — sudosubin/agents.nix0 reactions
- Oct 26, 2026codex-marketplaces.github.builderio.agent-native: 1fe5d77 → 8505239 — sudosubin/agents.nix0 reactions
- Oct 26, 2026codex-plugins.github.builderio.agent-native: 1fe5d77 → 8505239 — sudosubin/agents.nix0 reactions
- Oct 26, 2026agent-plugins.github.builderio.agent-native: 1fe5d77 → 8505239 — sudosubin/agents.nix0 reactions
- Oct 22, 2026chore: update Agent Native exported skills — BuilderIO/skills0 reactions · 1 comment
- Oct 16, 2026[Candidate] builderio/agent-native - large collection (876 SKILL.md files) — ProSkillsMD/proskills0 reactions · 1 comment
- Oct 15, 2026Builder.io, 에이전틱 앱 오픈소스 TS 프레임워크 'Agent-Native' 베타 출시 — UI·에이전트·HTTP·CLI·MCP·A2A 6개 서피스 공유 액션 (10/15, 공식 블로그) — roomedia/ax-trend0 reactions
- Oct 15, 2026Define human decisions once as schema-checked actions shared by the dashboard and factory mcp — mikeroySoft/factory0 reactions · 9 comments
On Bluesky · 11
- Sep 20, 2026📦 BuilderIO / agent-native ⭐ 4,978 (+89) 🗒 TypeScript A framework for building agentic apps — @github-trending-js.bsky.social2 likes and reposts
- Jul 26, 2026📦 BuilderIO / agent-native ⭐ 4,011 (+96) 🗒 TypeScript A framework for building agent-native applications. — @github-trending-js.bsky.social2 likes and reposts
- Jun 20, 2026agent-native: a TypeScript framework from Builder.io for apps where AI agents are first-class users, not an afterthought. Design once,… — @epicvibecoder.bsky.social2 likes and reposts · 1 reply
- Oct 16, 2026🔥 Hot Repo! 🔥 (100+ new stars) 📦 BuilderIO / agent-native ⭐ 7,378 (+107) 🗒 TypeScript A framework for building agentic apps — @github-trending-js.bsky.social1 likes and repost
- Sep 23, 2026今日のGitHubトレンド BuilderIO/agent-native Agent-Nativeは、自律的に動作するエージェントと専用UIを組み合わせて開発するためのTypeScriptフレームワークです。… — @dailygithubtrends.bsky.social1 likes and repost
6 more on Bluesky
- Sep 20, 2026📦 BuilderIO / agent-native ⭐ 4,978 (+89) 🗒 TypeScript A framework for building agentic apps — @github-trending.bsky.social1 likes and repost
- Sep 20, 2026今日のGitHubトレンド BuilderIO/agent-native Agent-Nativeは、自律的な動作を行うエージェントと専用UIを統合して構築するためのTypeScriptフレームワークです。… — @dailygithubtrends.bsky.social1 likes and repost
- Jun 19, 2026今日のGitHubトレンド BuilderIO/agent-native Agent-Nativeは、リッチなUIと自律型エージェントを統合したアプリケーションを構築するためのオープンソースフレームワークです。… — @dailygithubtrends.bsky.social1 likes and repost
- Jun 19, 2026🔥 Hot Repo! 🔥 (100+ new stars) 📦 BuilderIO / agent-native ⭐ 819 (+172) 🗒 TypeScript A framework for building agent-native applications. — @github-trending.bsky.social1 likes and repost
- Jun 18, 2026🔥 Hot Repo! 🔥 (100+ new stars) 📦 BuilderIO / agent-native ⭐ 673 (+131) 🗒 TypeScript A framework for building agent-native applications. — @github-trending-js.bsky.social1 likes and repost
- Jun 23, 2026🚀 Skyrocketing! 🚀 (200+ new stars) 📦 BuilderIO / agent-native ⭐ 1,793 (+434) 🗒 TypeScript A framework for building agent-native… — @github-trending-js.bsky.social0 likes and reposts · 1 reply
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- Anglesite/anglesite-skills★ 15same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository103 other files it gives its agents.
- AGENTS.mdAGENTS.md
- a2a-protocolSkill
- actionsSkill
- adding-a-featureSkill
- adding-tests-and-ciSkill
- adding-workspace-appsSkill
- address-feedbackSkill
- address-feedback-with-repliesSkill
- agent-native-docsSkill
- agent-native-toolkitSkill
- agent-pageSkill
- audit-logSkill
- authenticationSkill
- automationsSkill
- babysit-prSkill
- browser-sessionsSkill
- bug-traceSkill
- build-an-appSkill
- capture-learningsSkill
- changelogSkill
- chat-first-workbenchSkill
- client-methodsSkill
- client-side-routingSkill
- community-prsSkill
- composable-mini-appsSkill
- concurrent-agentsSkill
- configurationSkill
- context-awarenessSkill
- context-xraySkill
- create-skillSkill
- customizing-agent-nativeSkill
- data-programsSkill
- debug-runsSkill
- decisionsSkill
- delegate-to-agentSkill
- delegating-workSkill
- design-editor-reproSkill
- extension-pointsSkill
- extensionsSkill
- external-agentsSkill
- feature-flagsSkill
- fix-at-the-boundarySkill
- fragility-commonSkill
- framework-storesSkill
- frontend-designSkill
- generative-uiSkill
- harness-agentsSkill
- identify-fragile-systemsSkill
- integration-webhooksSkill
- internationalizationSkill
- investigate-bugSkill
- jira-refactor-findingsSkill
- launchdarklySkill
- multi-frontier-desktopSkill
- mvp-followupSkill
- native-navigationSkill
- new-branchSkill
- observabilitySkill
- onboardingSkill
- performanceSkill
- The first 60 shown.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

