@agent-native/core 0.144.1 → 0.144.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/corpus/templates/analytics/app/pages/DataSources.tsx +28 -4
  2. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +7 -3
  3. package/corpus/templates/analytics/server/plugins/dashboard-report-jobs.ts +13 -3
  4. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +64 -23
  5. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +69 -46
  6. package/corpus/templates/calendar/app/hooks/use-view-preferences.ts +35 -6
  7. package/corpus/templates/calendar/app/hooks/use-zoom-auth.ts +24 -3
  8. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +45 -6
  9. package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +38 -5
  10. package/corpus/templates/clips/desktop/src/app.tsx +146 -45
  11. package/corpus/templates/clips/server/jobs/brain-export.ts +13 -6
  12. package/corpus/templates/clips/server/jobs/builder-media-compression.ts +10 -3
  13. package/corpus/templates/clips/server/jobs/meeting-reminders.ts +10 -3
  14. package/corpus/templates/clips/server/jobs/poll-calendars.ts +23 -21
  15. package/corpus/templates/clips/server/jobs/stale-meeting-sweeper.ts +10 -3
  16. package/corpus/templates/clips/server/jobs/transactional-emails.ts +10 -3
  17. package/corpus/templates/content/app/components/sidebar/NotionButton.tsx +1 -0
  18. package/corpus/templates/design/app/components/design/FusionAppBanner.tsx +15 -2
  19. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +82 -39
  20. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +31 -9
  21. package/corpus/templates/mail/server/plugins/mail-jobs.ts +36 -17
  22. package/dist/client/AssistantChat.js +73 -43
  23. package/dist/client/MultiTabAssistantChat.d.ts +3 -1
  24. package/dist/client/MultiTabAssistantChat.js +79 -92
  25. package/dist/client/analytics/index.d.ts +1 -1
  26. package/dist/client/analytics/index.js +1 -1
  27. package/dist/client/analytics-session.d.ts +13 -0
  28. package/dist/client/analytics-session.js +57 -0
  29. package/dist/client/analytics.d.ts +1 -0
  30. package/dist/client/analytics.js +1 -0
  31. package/dist/client/browser-session-bridge.js +97 -54
  32. package/dist/client/db-admin/useAgentSync.js +34 -43
  33. package/dist/client/dynamic-suggestions.d.ts +3 -0
  34. package/dist/client/dynamic-suggestions.js +54 -52
  35. package/dist/client/hooks/index.d.ts +1 -0
  36. package/dist/client/hooks/index.js +1 -0
  37. package/dist/client/i18n.d.ts +7 -0
  38. package/dist/client/i18n.js +12 -3
  39. package/dist/client/index.d.ts +1 -1
  40. package/dist/client/index.js +1 -1
  41. package/dist/client/integrations/useIntegrationStatus.d.ts +1 -1
  42. package/dist/client/integrations/useIntegrationStatus.js +9 -22
  43. package/dist/client/notifications/NotificationsBell.js +14 -9
  44. package/dist/client/progress/RunsTray.js +6 -5
  45. package/dist/client/settings/useBuilderStatus.js +95 -73
  46. package/dist/client/track.js +7 -0
  47. package/dist/client/transcription/BuilderTranscriptionCta.js +27 -31
  48. package/dist/client/use-action.js +10 -1
  49. package/dist/client/use-agent-engine-configured.js +21 -3
  50. package/dist/client/use-pausing-interval.d.ts +6 -0
  51. package/dist/client/use-pausing-interval.js +6 -0
  52. package/dist/client/use-poll-loop.d.ts +39 -0
  53. package/dist/client/use-poll-loop.js +73 -0
  54. package/dist/client/use-run-stuck-detection.js +41 -13
  55. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  56. package/dist/integrations/google-docs-poller.js +26 -7
  57. package/dist/integrations/pending-tasks-retry-job.js +17 -23
  58. package/dist/integrations/plugin.js +16 -15
  59. package/dist/integrations/remote-push-delivery-job.js +16 -25
  60. package/dist/integrations/remote-retry-job.js +19 -23
  61. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  62. package/dist/notifications/routes.d.ts +3 -3
  63. package/dist/oauth-tokens/google-refresh.js +12 -19
  64. package/dist/observability/routes.d.ts +3 -3
  65. package/dist/progress/routes.d.ts +1 -1
  66. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  67. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  68. package/dist/resources/handlers.d.ts +1 -1
  69. package/dist/secrets/routes.d.ts +3 -3
  70. package/dist/server/action-routes.js +4 -2
  71. package/dist/server/agent-chat-plugin.js +26 -29
  72. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  73. package/dist/server/agent-run-context.d.ts +4 -2
  74. package/dist/server/agent-run-context.js +5 -3
  75. package/dist/server/core-routes-plugin.js +2 -0
  76. package/dist/server/index.d.ts +1 -0
  77. package/dist/server/index.js +1 -0
  78. package/dist/server/interval-job.d.ts +25 -0
  79. package/dist/server/interval-job.js +19 -0
  80. package/dist/shared/index.d.ts +1 -0
  81. package/dist/shared/index.js +1 -0
  82. package/dist/shared/poll-engine.d.ts +49 -0
  83. package/dist/shared/poll-engine.js +148 -0
  84. package/dist/templates/default/.agents/skills/internationalization/SKILL.md +5 -0
  85. package/dist/templates/workspace-core/.agents/skills/internationalization/SKILL.md +5 -0
  86. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +5 -1
  87. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +20 -3
  88. package/dist/tracking/index.d.ts +1 -1
  89. package/dist/tracking/providers.js +22 -2
  90. package/dist/tracking/registry.d.ts +12 -2
  91. package/dist/tracking/registry.js +27 -3
  92. package/dist/tracking/types.d.ts +8 -0
  93. package/docs/content/observability.mdx +16 -0
  94. package/docs/content/tracking.mdx +17 -2
  95. package/package.json +4 -3
  96. package/src/templates/default/.agents/skills/internationalization/SKILL.md +5 -0
  97. package/src/templates/workspace-core/.agents/skills/internationalization/SKILL.md +5 -0
  98. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +5 -1
  99. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +20 -3
@@ -283,7 +283,11 @@ same best-effort fan-out as other tracking events.
283
283
  upward through `$ai_parent_id` so a backend can rebuild the tree.
284
284
  `$ai_session_id` is the thread; the browser session is separate and ships as
285
285
  `$session_id`, read from `X-Agent-Native-Session-Id` via
286
- `RequestContext.browserSessionId`. Emission lives in `posthog-ai.ts`.
286
+ `RequestContext.browserSessionId`. The agent chat and the action client both
287
+ send that header, so a UI action call and the agent's own call during one
288
+ visit share a session. `setAnalyticsSessionId()` from
289
+ `@agent-native/core/client/analytics` pins a custom id and opts it out of the
290
+ 30-minute idle rotation. Emission lives in `posthog-ai.ts`.
287
291
  - Agent Native Analytics shape: the same event lands in `analytics_events` with
288
292
  mirrored query-friendly properties such as `run_id`, `thread_id`,
289
293
  `cost_cents_x100`, `duration_ms`, `tool_calls`, `successful_tools`,
@@ -18,19 +18,26 @@ The tracking system provides a single `track()` call that fans out to all regist
18
18
  ## How It Works
19
19
 
20
20
  1. At server startup, `registerBuiltinProviders()` checks env vars and registers any configured providers.
21
- 2. Application code calls `track(eventName, properties, meta)` from actions, plugins, or server routes.
21
+ 2. Application code calls `track(eventName, properties, source)` from actions, plugins, or server routes.
22
22
  3. The registry fans out the event to every registered provider. Errors are caught and logged -- a failing provider never crashes the caller.
23
23
  4. Built-in providers batch HTTP calls (flush every 10 seconds or 50 events, whichever comes first).
24
24
 
25
25
  ## API
26
26
 
27
- ### `track(name, properties?, meta?)`
27
+ ### `track(name, properties?, source?)`
28
28
 
29
- Fire an analytics event.
29
+ Fire an analytics event. `source` is either a `{ userId, anonymousId, sessionId }`
30
+ meta object or an action's `ctx` passed straight through.
30
31
 
31
32
  ```ts
32
33
  import { track } from "@agent-native/core/tracking";
33
34
 
35
+ // From an action — pass ctx; userId comes from ctx.userEmail.
36
+ run: async ({ name }, ctx) => {
37
+ track("meal.logged", { mealName: name, calories: 350 }, ctx);
38
+ };
39
+
40
+ // From a plugin or route with no ctx.
34
41
  track(
35
42
  "meal.logged",
36
43
  { mealName: "Salad", calories: 350 },
@@ -38,6 +45,16 @@ track(
38
45
  );
39
46
  ```
40
47
 
48
+ The caller's browser session comes from the ambient request context
49
+ (`RequestContext.browserSessionId`, set from the `X-Agent-Native-Session-Id`
50
+ header), so it resolves the same whether the UI called the action or the agent
51
+ did. Pass `sessionId` in the meta object to override it — routes that run
52
+ outside a request context, such as `/_agent-native/track`, do exactly that.
53
+ Providers map it to their own session field: `$session_id` for PostHog (which
54
+ joins the event to session replay), `session_id` as a property for Mixpanel and
55
+ Amplitude, a top-level `sessionId` for webhooks and Agent Native Analytics. It
56
+ is absent for callers with no browser — cron, CLI, MCP, A2A.
57
+
41
58
  ### `identify(userId, traits?)`
42
59
 
43
60
  Identify a user with traits. Forwarded to providers that support it.
@@ -1,4 +1,4 @@
1
- export { track, identify, flushTracking, registerTrackingProvider, unregisterTrackingProvider, listTrackingProviders, } from "./registry.js";
1
+ export { track, identify, flushTracking, registerTrackingProvider, unregisterTrackingProvider, listTrackingProviders, type TrackingMeta, type TrackingSource, } from "./registry.js";
2
2
  export { registerBuiltinProviders } from "./providers.js";
3
3
  export { captureException, type TrackingExceptionContext, type TrackingExceptionLevel, } from "./error-capture.js";
4
4
  export { errorToPostHogExceptionProperties, parseStackFrames, reshapeTrackedExceptionProperties, toPostHogExceptionProperties, type PostHogExceptionEntry, type PostHogExceptionInput, type PostHogExceptionLevel, type PostHogExceptionProperties, type PostHogStackFrame, } from "./posthog-exception.js";
@@ -38,7 +38,16 @@ function setTimer(t) {
38
38
  function enqueue(url, body, headers, options) {
39
39
  const queue = getQueue();
40
40
  queue.push({ url, body, headers });
41
- if (options?.flushImmediately || queue.length >= MAX_BATCH_SIZE) {
41
+ // The batch timer is unref'd so it never keeps a long-lived server alive —
42
+ // but on Netlify/Vercel/Lambda the execution environment can freeze the
43
+ // moment the event loop looks empty, which is exactly when an unref'd timer
44
+ // doesn't count. Without a signal, a warm container freezing between
45
+ // invocations never fires that timer, silently dropping every provider's
46
+ // queued events for a request that ends in a crash. Default to flushing
47
+ // synchronously with the response in that environment; a caller (e.g.
48
+ // Agent Native Analytics' flush-mode override) can still force either way.
49
+ const flushImmediately = options?.flushImmediately ?? isServerlessRuntime();
50
+ if (flushImmediately || queue.length >= MAX_BATCH_SIZE) {
42
51
  void drainQueue();
43
52
  }
44
53
  else if (!getTimer()) {
@@ -134,6 +143,7 @@ function createPostHogProvider(apiKey, host, errorTracking) {
134
143
  properties: {
135
144
  distinct_id: distinctId,
136
145
  ...properties,
146
+ ...(event.sessionId ? { $session_id: event.sessionId } : {}),
137
147
  timestamp: event.timestamp,
138
148
  },
139
149
  }));
@@ -168,6 +178,7 @@ function createPostHogProvider(apiKey, host, errorTracking) {
168
178
  distinct_id: distinctId,
169
179
  properties: {
170
180
  ...event.properties,
181
+ ...(event.sessionId ? { $session_id: event.sessionId } : {}),
171
182
  timestamp: event.timestamp,
172
183
  },
173
184
  }));
@@ -225,6 +236,9 @@ function createMixpanelProvider(token) {
225
236
  ? new Date(event.timestamp).getTime() / 1000
226
237
  : undefined,
227
238
  ...event.properties,
239
+ // Mixpanel's own `$session_id` is numeric and assigned by its SDK, so
240
+ // the browser session lands as a plain property here.
241
+ ...(event.sessionId ? { session_id: event.sessionId } : {}),
228
242
  },
229
243
  };
230
244
  enqueue("https://api.mixpanel.com/track", JSON.stringify([data]));
@@ -253,7 +267,11 @@ function createAmplitudeProvider(apiKey) {
253
267
  {
254
268
  event_type: event.name,
255
269
  user_id: event.userId || "anonymous",
256
- event_properties: event.properties,
270
+ // Amplitude's top-level `session_id` must be a numeric epoch, so
271
+ // the browser session ships as an event property instead.
272
+ event_properties: event.sessionId
273
+ ? { ...event.properties, session_id: event.sessionId }
274
+ : event.properties,
257
275
  time: event.timestamp
258
276
  ? new Date(event.timestamp).getTime()
259
277
  : undefined,
@@ -290,6 +308,7 @@ function createWebhookProvider(url, authHeader) {
290
308
  event: event.name,
291
309
  properties: event.properties,
292
310
  userId: event.userId,
311
+ sessionId: event.sessionId,
293
312
  timestamp: event.timestamp,
294
313
  }), extra);
295
314
  },
@@ -318,6 +337,7 @@ function createAgentNativeAnalyticsProvider(publicKey, endpoint) {
318
337
  properties: event.properties ?? {},
319
338
  userId: event.userId,
320
339
  anonymousId: event.anonymousId,
340
+ sessionId: event.sessionId,
321
341
  timestamp: event.timestamp,
322
342
  }), undefined, { flushImmediately });
323
343
  },
@@ -1,10 +1,20 @@
1
+ import type { ActionRunContext } from "../action.js";
1
2
  import type { TrackingProvider } from "./types.js";
2
3
  export declare function registerTrackingProvider(provider: TrackingProvider): void;
3
4
  export declare function unregisterTrackingProvider(name: string): boolean;
4
5
  export declare function listTrackingProviders(): string[];
5
- export declare function track(name: string, properties?: Record<string, unknown>, meta?: {
6
+ export interface TrackingMeta {
6
7
  userId?: string;
7
8
  anonymousId?: string;
8
- }): void;
9
+ /** Overrides the ambient request's browser session. */
10
+ sessionId?: string;
11
+ }
12
+ /**
13
+ * Who an event is attributed to. Pass an action's `ctx` straight through —
14
+ * `track("project_created", { template }, ctx)` — instead of restating
15
+ * `{ userId: ctx.userEmail }` at every call site.
16
+ */
17
+ export type TrackingSource = TrackingMeta | ActionRunContext;
18
+ export declare function track(name: string, properties?: Record<string, unknown>, source?: TrackingSource): void;
9
19
  export declare function identify(userId: string, traits?: Record<string, unknown>): void;
10
20
  export declare function flushTracking(): Promise<void[]>;
@@ -1,3 +1,4 @@
1
+ import { getRequestContext } from "../server/request-context.js";
1
2
  const REGISTRY_KEY = Symbol.for("@agent-native/core/tracking.registry");
2
3
  function getRegistry() {
3
4
  const g = globalThis;
@@ -20,13 +21,36 @@ export function unregisterTrackingProvider(name) {
20
21
  export function listTrackingProviders() {
21
22
  return Array.from(getRegistry().keys());
22
23
  }
23
- export function track(name, properties, meta) {
24
+ // `caller` is required on ActionRunContext and absent from TrackingMeta, so it
25
+ // is the one field that tells the two apart without the caller declaring which
26
+ // shape it passed.
27
+ function isActionRunContext(source) {
28
+ return typeof source.caller === "string";
29
+ }
30
+ function resolveTrackingSource(source) {
31
+ // The browser session rides the request, not the caller's arguments, so it
32
+ // resolves the same way whether the UI called the action or the agent did.
33
+ const ambientSessionId = getRequestContext()?.browserSessionId;
34
+ if (!source)
35
+ return { sessionId: ambientSessionId };
36
+ if (isActionRunContext(source)) {
37
+ return { userId: source.userEmail, sessionId: ambientSessionId };
38
+ }
39
+ return {
40
+ userId: source.userId,
41
+ anonymousId: source.anonymousId,
42
+ sessionId: source.sessionId ?? ambientSessionId,
43
+ };
44
+ }
45
+ export function track(name, properties, source) {
46
+ const { userId, anonymousId, sessionId } = resolveTrackingSource(source);
24
47
  const event = {
25
48
  name,
26
49
  properties,
27
50
  timestamp: new Date().toISOString(),
28
- userId: meta?.userId,
29
- anonymousId: meta?.anonymousId,
51
+ userId,
52
+ anonymousId,
53
+ sessionId,
30
54
  };
31
55
  for (const provider of getRegistry().values()) {
32
56
  try {
@@ -4,6 +4,14 @@ export interface TrackingEvent {
4
4
  timestamp?: string;
5
5
  userId?: string;
6
6
  anonymousId?: string;
7
+ /**
8
+ * Browser session the event belongs to, so a server-side event joins the
9
+ * visit that caused it. Carried as a typed field rather than a property so
10
+ * each provider maps it to its own session concept instead of every backend
11
+ * receiving another backend's reserved key. `undefined` for callers with no
12
+ * browser — cron, CLI, MCP, A2A — which is a real distinction, not a gap.
13
+ */
14
+ sessionId?: string;
7
15
  }
8
16
  export interface TrackingProvider {
9
17
  name: string;
@@ -307,6 +307,22 @@ Nodes share the run id as `$ai_trace_id`, so PostHog renders one tree per run.
307
307
  `$ai_session_id` is the conversation thread; the browser session is sent
308
308
  separately as `$session_id` so a trace links to its session replay.
309
309
 
310
+ The browser session id travels on the `X-Agent-Native-Session-Id` header. Both
311
+ the agent chat and the action client (`useActionQuery` / `useActionMutation` /
312
+ `callAction`) send it, and the server exposes it to actions and agent runs alike
313
+ as `RequestContext.browserSessionId` — so an action the UI calls and an action
314
+ the agent calls during the same visit correlate. The id rotates after 30 idle
315
+ minutes by default; to correlate a workflow under an id of your own, pin it once
316
+ at startup:
317
+
318
+ ```ts
319
+ import { setAnalyticsSessionId } from "@agent-native/core/client/analytics";
320
+
321
+ // Pinned ids opt out of idle rotation. Call clearAnalyticsSessionId() to
322
+ // return to the rotating default.
323
+ setAnalyticsSessionId(myWorkflowId);
324
+ ```
325
+
310
326
  Tool calls always appear (PostHog derives its tool tags from them), but their
311
327
  **arguments** require `captureToolArgs` and message content requires
312
328
  `capturePrompts`. When those are off the fields are omitted entirely rather than
@@ -116,7 +116,7 @@ used as a fallback for it.
116
116
 
117
117
  ## API {#api}
118
118
 
119
- ### `track(name, properties?, meta?)` {#track}
119
+ ### `track(name, properties?, source?)` {#track}
120
120
 
121
121
  Fire an analytics event. Fans out to all registered providers.
122
122
 
@@ -130,6 +130,19 @@ track(
130
130
  );
131
131
  ```
132
132
 
133
+ `source` is either an action's `ctx`, passed straight through, or a
134
+ `{ userId, anonymousId, sessionId }` meta object for callers that have no `ctx`.
135
+
136
+ The browser session is not something the caller has to supply: it comes from the
137
+ ambient request context (`RequestContext.browserSessionId`, set from the
138
+ `X-Agent-Native-Session-Id` header the action client, chat, and client `track()`
139
+ all send), so the same visit correlates whether the UI or the agent triggered the
140
+ event. Each provider maps it to its own session field — `$session_id` for
141
+ PostHog, which joins the event to session replay; a `session_id` property for
142
+ Mixpanel and Amplitude; a top-level `sessionId` for webhooks and Agent Native
143
+ Analytics. It is absent for callers with no browser (cron, CLI, MCP, A2A), and a
144
+ `sessionId` in the meta object overrides it.
145
+
133
146
  ### `identify(userId, traits?)` {#identify}
134
147
 
135
148
  Identify a user with traits. Forwarded to providers that support it (PostHog, Mixpanel, Amplitude, webhook).
@@ -166,7 +179,9 @@ export default defineAction({
166
179
  .values({ name, template })
167
180
  .returning();
168
181
 
169
- track("project.created", { name, template }, { userId: ctx?.userEmail });
182
+ // Pass ctx straight through: the event is attributed to ctx.userEmail and
183
+ // joins the browser session the call came from.
184
+ track("project.created", { name, template }, ctx);
170
185
 
171
186
  return { ok: true, projectId: project.id };
172
187
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.144.1",
3
+ "version": "0.144.3",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -72,6 +72,7 @@
72
72
  "./server/request-context": "./dist/server/request-context.js",
73
73
  "./server/design-token-utils": "./dist/server/design-token-utils.js",
74
74
  "./server/poll": "./dist/server/poll.js",
75
+ "./server/interval-job": "./dist/server/interval-job.js",
75
76
  "./server/sse": "./dist/server/sse.js",
76
77
  "./server/short-lived-token": "./dist/server/short-lived-token.js",
77
78
  "./integrations": "./dist/integrations/index.js",
@@ -411,8 +412,8 @@
411
412
  "y-protocols": "^1.0.7",
412
413
  "yjs": "^13.6.31",
413
414
  "zod": "^4.3.6",
414
- "@agent-native/recap-cli": "0.5.3",
415
- "@agent-native/toolkit": "^0.13.3"
415
+ "@agent-native/toolkit": "^0.13.3",
416
+ "@agent-native/recap-cli": "0.5.3"
416
417
  },
417
418
  "devDependencies": {
418
419
  "@ai-sdk/anthropic": "^3.0.71",
@@ -29,6 +29,11 @@ Templates use `app/i18n/`:
29
29
  Use BCP-47 filenames from the supported set: `en-US`, `zh-CN`, `es-ES`,
30
30
  `fr-FR`, `de-DE`, `ja-JP`, `ko-KR`, `pt-BR`, `hi-IN`, `ar-SA`.
31
31
 
32
+ If an app only ships translations for a subset of that set, set
33
+ `supportedLocales` on the `AgentNativeI18nCatalog` to that subset. Without it,
34
+ `<LanguagePicker />` lists every framework-supported locale regardless of
35
+ whether `loadMessages` can actually resolve it.
36
+
32
37
  ## UI Pattern
33
38
 
34
39
  - Wrap apps with `AppProviders i18n={{ catalog: i18nCatalog }}`.
@@ -29,6 +29,11 @@ Templates use `app/i18n/`:
29
29
  Use BCP-47 filenames from the supported set: `en-US`, `zh-CN`, `es-ES`,
30
30
  `fr-FR`, `de-DE`, `ja-JP`, `ko-KR`, `pt-BR`, `hi-IN`, `ar-SA`.
31
31
 
32
+ If an app only ships translations for a subset of that set, set
33
+ `supportedLocales` on the `AgentNativeI18nCatalog` to that subset. Without it,
34
+ `<LanguagePicker />` lists every framework-supported locale regardless of
35
+ whether `loadMessages` can actually resolve it.
36
+
32
37
  ## UI Pattern
33
38
 
34
39
  - Wrap apps with `AppProviders i18n={{ catalog: i18nCatalog }}`.
@@ -283,7 +283,11 @@ same best-effort fan-out as other tracking events.
283
283
  upward through `$ai_parent_id` so a backend can rebuild the tree.
284
284
  `$ai_session_id` is the thread; the browser session is separate and ships as
285
285
  `$session_id`, read from `X-Agent-Native-Session-Id` via
286
- `RequestContext.browserSessionId`. Emission lives in `posthog-ai.ts`.
286
+ `RequestContext.browserSessionId`. The agent chat and the action client both
287
+ send that header, so a UI action call and the agent's own call during one
288
+ visit share a session. `setAnalyticsSessionId()` from
289
+ `@agent-native/core/client/analytics` pins a custom id and opts it out of the
290
+ 30-minute idle rotation. Emission lives in `posthog-ai.ts`.
287
291
  - Agent Native Analytics shape: the same event lands in `analytics_events` with
288
292
  mirrored query-friendly properties such as `run_id`, `thread_id`,
289
293
  `cost_cents_x100`, `duration_ms`, `tool_calls`, `successful_tools`,
@@ -18,19 +18,26 @@ The tracking system provides a single `track()` call that fans out to all regist
18
18
  ## How It Works
19
19
 
20
20
  1. At server startup, `registerBuiltinProviders()` checks env vars and registers any configured providers.
21
- 2. Application code calls `track(eventName, properties, meta)` from actions, plugins, or server routes.
21
+ 2. Application code calls `track(eventName, properties, source)` from actions, plugins, or server routes.
22
22
  3. The registry fans out the event to every registered provider. Errors are caught and logged -- a failing provider never crashes the caller.
23
23
  4. Built-in providers batch HTTP calls (flush every 10 seconds or 50 events, whichever comes first).
24
24
 
25
25
  ## API
26
26
 
27
- ### `track(name, properties?, meta?)`
27
+ ### `track(name, properties?, source?)`
28
28
 
29
- Fire an analytics event.
29
+ Fire an analytics event. `source` is either a `{ userId, anonymousId, sessionId }`
30
+ meta object or an action's `ctx` passed straight through.
30
31
 
31
32
  ```ts
32
33
  import { track } from "@agent-native/core/tracking";
33
34
 
35
+ // From an action — pass ctx; userId comes from ctx.userEmail.
36
+ run: async ({ name }, ctx) => {
37
+ track("meal.logged", { mealName: name, calories: 350 }, ctx);
38
+ };
39
+
40
+ // From a plugin or route with no ctx.
34
41
  track(
35
42
  "meal.logged",
36
43
  { mealName: "Salad", calories: 350 },
@@ -38,6 +45,16 @@ track(
38
45
  );
39
46
  ```
40
47
 
48
+ The caller's browser session comes from the ambient request context
49
+ (`RequestContext.browserSessionId`, set from the `X-Agent-Native-Session-Id`
50
+ header), so it resolves the same whether the UI called the action or the agent
51
+ did. Pass `sessionId` in the meta object to override it — routes that run
52
+ outside a request context, such as `/_agent-native/track`, do exactly that.
53
+ Providers map it to their own session field: `$session_id` for PostHog (which
54
+ joins the event to session replay), `session_id` as a property for Mixpanel and
55
+ Amplitude, a top-level `sessionId` for webhooks and Agent Native Analytics. It
56
+ is absent for callers with no browser — cron, CLI, MCP, A2A.
57
+
41
58
  ### `identify(userId, traits?)`
42
59
 
43
60
  Identify a user with traits. Forwarded to providers that support it.