@agent-native/core 0.144.1 → 0.144.2

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 (37) hide show
  1. package/dist/client/analytics/index.d.ts +1 -1
  2. package/dist/client/analytics/index.js +1 -1
  3. package/dist/client/analytics-session.d.ts +13 -0
  4. package/dist/client/analytics-session.js +57 -0
  5. package/dist/client/analytics.d.ts +1 -0
  6. package/dist/client/analytics.js +1 -0
  7. package/dist/client/index.d.ts +1 -1
  8. package/dist/client/index.js +1 -1
  9. package/dist/client/track.js +7 -0
  10. package/dist/client/use-action.js +10 -1
  11. package/dist/collab/routes.d.ts +1 -1
  12. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  13. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  14. package/dist/notifications/routes.d.ts +4 -4
  15. package/dist/observability/routes.d.ts +3 -3
  16. package/dist/progress/routes.d.ts +1 -1
  17. package/dist/provider-api/actions/custom-provider-registration.d.ts +2 -2
  18. package/dist/resources/handlers.d.ts +1 -1
  19. package/dist/secrets/routes.d.ts +3 -3
  20. package/dist/server/action-routes.js +4 -2
  21. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  22. package/dist/server/agent-run-context.d.ts +4 -2
  23. package/dist/server/agent-run-context.js +5 -3
  24. package/dist/server/core-routes-plugin.js +2 -0
  25. package/dist/server/realtime-token.d.ts +1 -1
  26. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +5 -1
  27. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +20 -3
  28. package/dist/tracking/index.d.ts +1 -1
  29. package/dist/tracking/providers.js +12 -1
  30. package/dist/tracking/registry.d.ts +12 -2
  31. package/dist/tracking/registry.js +27 -3
  32. package/dist/tracking/types.d.ts +8 -0
  33. package/docs/content/observability.mdx +16 -0
  34. package/docs/content/tracking.mdx +17 -2
  35. package/package.json +1 -1
  36. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +5 -1
  37. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +20 -3
@@ -1,2 +1,2 @@
1
- export { AGENT_NATIVE_EXCEPTION_EVENT_NAME, addErrorBreadcrumb, captureClientException, captureError, captureException, captureMessage, configureTracking, getAnalyticsAnonymousId, getAnalyticsSessionId, getFirstTouchAttribution, getSessionReplayContext, getSessionReplayUrl, isErrorCaptureInstalled, maybeStartSessionReplay, setSentryUser, setTrackingContentCaptureEnabled, setTrackingIdentity, startSessionReplay, stopSessionReplay, trackAgentChatLifecycle, trackEvent, trackSessionStatus, type AgentChatLifecycleEvent, type CaptureExceptionContext, type CapturedExceptionEvent, type ClientCaptureContext, type ConfigureTrackingOptions, type ErrorCaptureConfigOptions, type ExceptionBreadcrumb, type ExceptionLevel, type FirstTouchAttribution, type SessionReplayContext, type SessionReplayLinkOptions, type SessionReplayOptions, type SessionReplayStartResult, type SessionReplayUrlMatcher, type TrackingIdentityUser, } from "../analytics.js";
1
+ export { AGENT_NATIVE_EXCEPTION_EVENT_NAME, addErrorBreadcrumb, captureClientException, captureError, captureException, captureMessage, clearAnalyticsSessionId, configureTracking, getAnalyticsAnonymousId, getAnalyticsSessionId, getFirstTouchAttribution, getSessionReplayContext, getSessionReplayUrl, isErrorCaptureInstalled, maybeStartSessionReplay, setAnalyticsSessionId, setSentryUser, setTrackingContentCaptureEnabled, setTrackingIdentity, startSessionReplay, stopSessionReplay, trackAgentChatLifecycle, trackEvent, trackSessionStatus, type AgentChatLifecycleEvent, type CaptureExceptionContext, type CapturedExceptionEvent, type ClientCaptureContext, type ConfigureTrackingOptions, type ErrorCaptureConfigOptions, type ExceptionBreadcrumb, type ExceptionLevel, type FirstTouchAttribution, type SessionReplayContext, type SessionReplayLinkOptions, type SessionReplayOptions, type SessionReplayStartResult, type SessionReplayUrlMatcher, type TrackingIdentityUser, } from "../analytics.js";
2
2
  export { track } from "../track.js";
@@ -1,2 +1,2 @@
1
- export { AGENT_NATIVE_EXCEPTION_EVENT_NAME, addErrorBreadcrumb, captureClientException, captureError, captureException, captureMessage, configureTracking, getAnalyticsAnonymousId, getAnalyticsSessionId, getFirstTouchAttribution, getSessionReplayContext, getSessionReplayUrl, isErrorCaptureInstalled, maybeStartSessionReplay, setSentryUser, setTrackingContentCaptureEnabled, setTrackingIdentity, startSessionReplay, stopSessionReplay, trackAgentChatLifecycle, trackEvent, trackSessionStatus, } from "../analytics.js";
1
+ export { AGENT_NATIVE_EXCEPTION_EVENT_NAME, addErrorBreadcrumb, captureClientException, captureError, captureException, captureMessage, clearAnalyticsSessionId, configureTracking, getAnalyticsAnonymousId, getAnalyticsSessionId, getFirstTouchAttribution, getSessionReplayContext, getSessionReplayUrl, isErrorCaptureInstalled, maybeStartSessionReplay, setAnalyticsSessionId, setSentryUser, setTrackingContentCaptureEnabled, setTrackingIdentity, startSessionReplay, stopSessionReplay, trackAgentChatLifecycle, trackEvent, trackSessionStatus, } from "../analytics.js";
2
2
  export { track } from "../track.js";
@@ -1,2 +1,15 @@
1
1
  export declare function getOrCreateAnalyticsAnonymousId(): string | undefined;
2
+ /**
3
+ * Pin the browser session id an app wants every analytics event, agent run, and
4
+ * action request in this tab to carry. Returns the stored id, or `undefined`
5
+ * when there is no browser storage to pin it in (SSR) — the caller can tell
6
+ * those apart.
7
+ *
8
+ * Throws on an id the transport cannot carry rather than storing it.
9
+ */
10
+ export declare function setAnalyticsSessionId(sessionId: string): string | undefined;
11
+ /**
12
+ * Drop a pinned id and let the next read start a fresh rotating session.
13
+ */
14
+ export declare function clearAnalyticsSessionId(): void;
2
15
  export declare function getOrCreateAnalyticsSessionId(): string | undefined;
@@ -1,10 +1,17 @@
1
1
  import { serializeAnalyticsAnonymousIdCookie } from "../shared/analytics-anonymous-id.js";
2
2
  const ANONYMOUS_ID_STORAGE_KEY = "agent-native.anonymous_id";
3
3
  const SESSION_ID_STORAGE_KEY = "agent-native.session_id";
4
+ const SESSION_ID_PIN_STORAGE_KEY = "agent-native.session_id_pin";
4
5
  const SESSION_LAST_ACTIVITY_STORAGE_KEY = "agent-native.session_last_activity";
5
6
  // 30-minute idle timeout matches GA4 / Mixpanel defaults: a tab left open
6
7
  // overnight starts a new session in the morning instead of stretching one visit.
7
8
  const SESSION_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
9
+ // The server drops the session header at 128 characters
10
+ // (`readBrowserSessionIdHeader`), and `fetch` throws on a header value with
11
+ // non-ASCII or newline bytes. Both are rejected at the point the id is set so a
12
+ // bad id fails once, loudly, instead of silently unlinking every later request.
13
+ const MAX_SESSION_ID_LENGTH = 127;
14
+ const SAFE_SESSION_ID = /^[!-~]+$/;
8
15
  function generateVisitorId() {
9
16
  try {
10
17
  if (typeof crypto !== "undefined" &&
@@ -35,6 +42,15 @@ function safeStorageSet(key, value) {
35
42
  // private browsing / storage disabled -- best-effort
36
43
  }
37
44
  }
45
+ function safeStorageRemove(key) {
46
+ try {
47
+ window.localStorage.removeItem(key);
48
+ // coercion-ok: storage that refuses writes holds no key to clear, so the caller's end state already holds
49
+ }
50
+ catch {
51
+ // private browsing / storage disabled -- best-effort
52
+ }
53
+ }
38
54
  function syncAnalyticsAnonymousIdCookie(id) {
39
55
  try {
40
56
  const cookie = serializeAnalyticsAnonymousIdCookie(id);
@@ -57,10 +73,51 @@ export function getOrCreateAnalyticsAnonymousId() {
57
73
  syncAnalyticsAnonymousIdCookie(id);
58
74
  return id;
59
75
  }
76
+ /**
77
+ * Pin the browser session id an app wants every analytics event, agent run, and
78
+ * action request in this tab to carry. Returns the stored id, or `undefined`
79
+ * when there is no browser storage to pin it in (SSR) — the caller can tell
80
+ * those apart.
81
+ *
82
+ * Throws on an id the transport cannot carry rather than storing it.
83
+ */
84
+ export function setAnalyticsSessionId(sessionId) {
85
+ const trimmed = typeof sessionId === "string" ? sessionId.trim() : "";
86
+ if (!trimmed ||
87
+ trimmed.length > MAX_SESSION_ID_LENGTH ||
88
+ !SAFE_SESSION_ID.test(trimmed)) {
89
+ throw new Error(`Invalid analytics session id: expected 1-${MAX_SESSION_ID_LENGTH} printable ASCII characters with no whitespace`);
90
+ }
91
+ if (typeof window === "undefined")
92
+ return undefined;
93
+ safeStorageSet(SESSION_ID_PIN_STORAGE_KEY, trimmed);
94
+ safeStorageSet(SESSION_ID_STORAGE_KEY, trimmed);
95
+ safeStorageSet(SESSION_LAST_ACTIVITY_STORAGE_KEY, String(Date.now()));
96
+ return trimmed;
97
+ }
98
+ /**
99
+ * Drop a pinned id and let the next read start a fresh rotating session.
100
+ */
101
+ export function clearAnalyticsSessionId() {
102
+ if (typeof window === "undefined")
103
+ return;
104
+ safeStorageRemove(SESSION_ID_PIN_STORAGE_KEY);
105
+ safeStorageRemove(SESSION_ID_STORAGE_KEY);
106
+ safeStorageRemove(SESSION_LAST_ACTIVITY_STORAGE_KEY);
107
+ }
60
108
  export function getOrCreateAnalyticsSessionId() {
61
109
  if (typeof window === "undefined")
62
110
  return undefined;
63
111
  const now = Date.now();
112
+ // A pinned id opts out of idle rotation: the app chose it to correlate its
113
+ // own runs, so expiring it mid-workflow would split one correlated session
114
+ // into two that nothing can join back together.
115
+ const pinned = safeStorageGet(SESSION_ID_PIN_STORAGE_KEY);
116
+ if (pinned) {
117
+ safeStorageSet(SESSION_ID_STORAGE_KEY, pinned);
118
+ safeStorageSet(SESSION_LAST_ACTIVITY_STORAGE_KEY, String(now));
119
+ return pinned;
120
+ }
64
121
  const lastActivityRaw = safeStorageGet(SESSION_LAST_ACTIVITY_STORAGE_KEY);
65
122
  const lastActivity = lastActivityRaw
66
123
  ? Number.parseInt(lastActivityRaw, 10)
@@ -1,3 +1,4 @@
1
+ export { clearAnalyticsSessionId, setAnalyticsSessionId, } from "./analytics-session.js";
1
2
  import type { SessionReplayOptions, SessionReplayStartResult } from "./session-replay.js";
2
3
  export { scrubUrl } from "./url-scrub.js";
3
4
  export { addErrorBreadcrumb, captureException, captureMessage, isErrorCaptureInstalled, type CaptureExceptionContext, type CapturedExceptionEvent, type ExceptionBreadcrumb, type ExceptionLevel, } from "./error-capture.js";
@@ -1,6 +1,7 @@
1
1
  import { llmConnectionTrackingProperties, } from "../shared/llm-connection.js";
2
2
  import { toPostHogExceptionProperties } from "../tracking/posthog-exception.js";
3
3
  import { getOrCreateAnalyticsAnonymousId, getOrCreateAnalyticsSessionId, } from "./analytics-session.js";
4
+ export { clearAnalyticsSessionId, setAnalyticsSessionId, } from "./analytics-session.js";
4
5
  import { fetchAgentEngineStatus, fetchAuthSessionStatus, } from "./client-status-requests.js";
5
6
  import { installErrorCapture, } from "./error-capture.js";
6
7
  import { isDynamicImportFailureMessage } from "./route-chunk-recovery.js";
@@ -18,7 +18,7 @@ export { DevDatabaseLink, type DevDatabaseLinkProps, } from "./db-admin/DevDatab
18
18
  export { DbAdminPage } from "./db-admin/DbAdminPage.js";
19
19
  export { installRouteChunkRecovery, reloadForStaleChunk, recoverFromStaleChunkError, } from "./route-chunk-recovery.js";
20
20
  export { AgentTerminal, type AgentTerminalProps } from "./terminal/index.js";
21
- export { trackEvent, trackAgentChatLifecycle, trackSessionStatus, configureTracking, setTrackingContentCaptureEnabled, maybeStartSessionReplay, startSessionReplay, stopSessionReplay, getAnalyticsAnonymousId, getAnalyticsSessionId, getFirstTouchAttribution, setSentryUser, setTrackingIdentity, getSessionReplayContext, getSessionReplayUrl, captureError, captureClientException, AGENT_NATIVE_EXCEPTION_EVENT_NAME, addErrorBreadcrumb, captureException, captureMessage, isErrorCaptureInstalled, type CaptureExceptionContext, type CapturedExceptionEvent, type ClientCaptureContext, type AgentChatLifecycleEvent, type ConfigureTrackingOptions, type ErrorCaptureConfigOptions, type ExceptionBreadcrumb, type ExceptionLevel, type FirstTouchAttribution, type SessionReplayOptions, type SessionReplayContext, type SessionReplayLinkOptions, type SessionReplayStartResult, type SessionReplayUrlMatcher, type TrackingIdentityUser, } from "./analytics.js";
21
+ export { trackEvent, trackAgentChatLifecycle, trackSessionStatus, configureTracking, setTrackingContentCaptureEnabled, maybeStartSessionReplay, startSessionReplay, stopSessionReplay, getAnalyticsAnonymousId, getAnalyticsSessionId, setAnalyticsSessionId, clearAnalyticsSessionId, getFirstTouchAttribution, setSentryUser, setTrackingIdentity, getSessionReplayContext, getSessionReplayUrl, captureError, captureClientException, AGENT_NATIVE_EXCEPTION_EVENT_NAME, addErrorBreadcrumb, captureException, captureMessage, isErrorCaptureInstalled, type CaptureExceptionContext, type CapturedExceptionEvent, type ClientCaptureContext, type AgentChatLifecycleEvent, type ConfigureTrackingOptions, type ErrorCaptureConfigOptions, type ExceptionBreadcrumb, type ExceptionLevel, type FirstTouchAttribution, type SessionReplayOptions, type SessionReplayContext, type SessionReplayLinkOptions, type SessionReplayStartResult, type SessionReplayUrlMatcher, type TrackingIdentityUser, } from "./analytics.js";
22
22
  export { track } from "./track.js";
23
23
  export { useCollaborativeDoc, isReconcileLeadClient, emailToColor, emailToName, dedupeCollabUsersByEmail, type UseCollaborativeDocOptions, type UseCollaborativeDocResult, type CollabUser, } from "../collab/client.js";
24
24
  export { AGENT_CLIENT_ID } from "../collab/agent-identity.js";
@@ -18,7 +18,7 @@ export { DevDatabaseLink, } from "./db-admin/DevDatabaseLink.js";
18
18
  export { DbAdminPage } from "./db-admin/DbAdminPage.js";
19
19
  export { installRouteChunkRecovery, reloadForStaleChunk, recoverFromStaleChunkError, } from "./route-chunk-recovery.js";
20
20
  export { AgentTerminal } from "./terminal/index.js";
21
- export { trackEvent, trackAgentChatLifecycle, trackSessionStatus, configureTracking, setTrackingContentCaptureEnabled, maybeStartSessionReplay, startSessionReplay, stopSessionReplay, getAnalyticsAnonymousId, getAnalyticsSessionId, getFirstTouchAttribution, setSentryUser, setTrackingIdentity, getSessionReplayContext, getSessionReplayUrl, captureError, captureClientException,
21
+ export { trackEvent, trackAgentChatLifecycle, trackSessionStatus, configureTracking, setTrackingContentCaptureEnabled, maybeStartSessionReplay, startSessionReplay, stopSessionReplay, getAnalyticsAnonymousId, getAnalyticsSessionId, setAnalyticsSessionId, clearAnalyticsSessionId, getFirstTouchAttribution, setSentryUser, setTrackingIdentity, getSessionReplayContext, getSessionReplayUrl, captureError, captureClientException,
22
22
  // First-party, Sentry-style error capture (auto + manual API).
23
23
  AGENT_NATIVE_EXCEPTION_EVENT_NAME, addErrorBreadcrumb, captureException, captureMessage, isErrorCaptureInstalled, } from "./analytics.js";
24
24
  export { track } from "./track.js";
@@ -1,3 +1,4 @@
1
+ import { getOrCreateAnalyticsSessionId } from "./analytics-session.js";
1
2
  import { agentNativePath } from "./api-path.js";
2
3
  /**
3
4
  * Fire an analytics event from browser/app code. This is the client-side twin
@@ -35,6 +36,7 @@ export function track(name, properties) {
35
36
  // Non-serializable properties — drop rather than throw into the caller.
36
37
  return Promise.resolve();
37
38
  }
39
+ const browserSessionId = getOrCreateAnalyticsSessionId();
38
40
  return fetch(agentNativePath("/_agent-native/track"), {
39
41
  method: "POST",
40
42
  headers: {
@@ -43,6 +45,11 @@ export function track(name, properties) {
43
45
  // middleware trusts it as a first-party marker. Matches the convention
44
46
  // used by other client writes (application-state, guided-questions).
45
47
  "X-Agent-Native-CSRF": "1",
48
+ // Same session the action client and agent chat send, so a client event
49
+ // and the server events from the same visit share one session.
50
+ ...(browserSessionId
51
+ ? { "X-Agent-Native-Session-Id": browserSessionId }
52
+ : {}),
46
53
  },
47
54
  body,
48
55
  keepalive: true,
@@ -22,6 +22,7 @@
22
22
  * compatibility.
23
23
  */
24
24
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
25
+ import { getOrCreateAnalyticsSessionId } from "./analytics-session.js";
25
26
  import { trackEvent } from "./analytics.js";
26
27
  import { agentNativePath } from "./api-path.js";
27
28
  import { getBrowserTabId } from "./browser-tab-id.js";
@@ -192,8 +193,16 @@ async function performActionFetch(name, method, params, options) {
192
193
  if (buildId)
193
194
  headers["X-Agent-Native-Build-Id"] = buildId;
194
195
  const tz = resolveUserTimezone();
195
- if (tz)
196
+ if (tz) {
196
197
  headers["x-user-timezone"] = tz;
198
+ }
199
+ // Same browser session the agent chat sends on an agent run, so an action
200
+ // called from the UI and an action the agent calls during that visit land on
201
+ // one `$session_id` in traces and session replay.
202
+ const browserSessionId = getOrCreateAnalyticsSessionId();
203
+ if (browserSessionId) {
204
+ headers["X-Agent-Native-Session-Id"] = browserSessionId;
205
+ }
197
206
  const init = {
198
207
  method,
199
208
  headers,
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
26
26
  * Body: { update: string (base64), requestSource?: string }
27
27
  */
28
28
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
29
- error: string;
30
29
  ok?: undefined;
30
+ error: string;
31
31
  } | {
32
32
  error?: undefined;
33
33
  ok: boolean;
@@ -17,11 +17,11 @@ declare const _default: import("../../action.js").ActionDefinition<{
17
17
  id?: undefined;
18
18
  provider?: undefined;
19
19
  } | {
20
- error?: undefined;
21
20
  configured?: undefined;
22
21
  connectPath?: undefined;
23
22
  url: string;
24
23
  id: string;
25
24
  provider: string;
25
+ error?: undefined;
26
26
  }>;
27
27
  export default _default;
@@ -137,13 +137,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
137
137
  inputSchema: {
138
138
  type: string;
139
139
  properties: {
140
- count?: undefined;
141
140
  query?: undefined;
142
141
  minutes?: undefined;
143
142
  limit?: undefined;
144
143
  clientHint?: undefined;
145
144
  timestamp?: undefined;
146
145
  chapterId?: undefined;
146
+ count?: undefined;
147
147
  startAt?: undefined;
148
148
  endAt?: undefined;
149
149
  reason?: undefined;
@@ -159,7 +159,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
159
159
  inputSchema: {
160
160
  type: string;
161
161
  properties: {
162
- count?: undefined;
163
162
  query: {
164
163
  type: string;
165
164
  description: string;
@@ -175,6 +174,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
175
174
  clientHint?: undefined;
176
175
  timestamp?: undefined;
177
176
  chapterId?: undefined;
177
+ count?: undefined;
178
178
  startAt?: undefined;
179
179
  endAt?: undefined;
180
180
  reason?: undefined;
@@ -190,7 +190,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
190
190
  inputSchema: {
191
191
  type: string;
192
192
  properties: {
193
- count?: undefined;
194
193
  minutes: {
195
194
  type: string;
196
195
  description: string;
@@ -200,6 +199,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
200
199
  clientHint?: undefined;
201
200
  timestamp?: undefined;
202
201
  chapterId?: undefined;
202
+ count?: undefined;
203
203
  startAt?: undefined;
204
204
  endAt?: undefined;
205
205
  reason?: undefined;
@@ -216,7 +216,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
216
216
  type: string;
217
217
  required: string[];
218
218
  properties: {
219
- count?: undefined;
220
219
  query: {
221
220
  type: string;
222
221
  description: string;
@@ -235,6 +234,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
235
234
  };
236
235
  timestamp?: undefined;
237
236
  chapterId?: undefined;
237
+ count?: undefined;
238
238
  startAt?: undefined;
239
239
  endAt?: undefined;
240
240
  reason?: undefined;
@@ -250,7 +250,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
250
250
  type: string;
251
251
  required: string[];
252
252
  properties: {
253
- count?: undefined;
254
253
  query?: undefined;
255
254
  minutes?: undefined;
256
255
  limit?: undefined;
@@ -264,6 +263,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
264
263
  description: string;
265
264
  };
266
265
  chapterId?: undefined;
266
+ count?: undefined;
267
267
  startAt?: undefined;
268
268
  endAt?: undefined;
269
269
  includeMicrophone?: undefined;
@@ -314,13 +314,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
314
314
  type: string;
315
315
  required: string[];
316
316
  properties: {
317
- count?: undefined;
318
317
  query?: undefined;
319
318
  minutes?: undefined;
320
319
  limit?: undefined;
321
320
  clientHint?: undefined;
322
321
  timestamp?: undefined;
323
322
  chapterId?: undefined;
323
+ count?: undefined;
324
324
  startAt: {
325
325
  type: string;
326
326
  description: string;
@@ -349,13 +349,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
349
349
  type: string;
350
350
  required: string[];
351
351
  properties: {
352
- count?: undefined;
353
352
  query?: undefined;
354
353
  minutes?: undefined;
355
354
  limit?: undefined;
356
355
  clientHint?: undefined;
357
356
  timestamp?: undefined;
358
357
  chapterId?: undefined;
358
+ count?: undefined;
359
359
  startAt?: undefined;
360
360
  endAt?: undefined;
361
361
  reason?: undefined;
@@ -16,18 +16,18 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
16
16
  error?: undefined;
17
17
  ok?: undefined;
18
18
  } | {
19
- count?: undefined;
20
19
  updated: number;
21
20
  error?: undefined;
22
21
  ok?: undefined;
23
- } | {
24
22
  count?: undefined;
23
+ } | {
25
24
  updated?: undefined;
26
25
  error: string;
27
26
  ok?: undefined;
28
- } | {
29
27
  count?: undefined;
28
+ } | {
30
29
  updated?: undefined;
31
- ok: boolean;
32
30
  error?: undefined;
31
+ ok: boolean;
32
+ count?: undefined;
33
33
  }>>;
@@ -42,22 +42,22 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
42
42
  avgEvalScore: number;
43
43
  } | {
44
44
  error?: undefined;
45
- ok?: undefined;
46
45
  summary: import("./types.js").TraceSummary;
47
46
  spans: import("./types.js").TraceSpan[];
48
47
  id?: undefined;
48
+ ok?: undefined;
49
49
  } | {
50
50
  error?: undefined;
51
- ok?: undefined;
52
51
  summary?: undefined;
53
52
  spans?: undefined;
54
53
  id: string;
55
- } | {
56
54
  ok?: undefined;
55
+ } | {
57
56
  summary?: undefined;
58
57
  spans?: undefined;
59
58
  id?: undefined;
60
59
  error: any;
60
+ ok?: undefined;
61
61
  } | {
62
62
  error?: undefined;
63
63
  summary?: undefined;
@@ -15,6 +15,6 @@ export declare function createProgressHandler(): import("h3").EventHandlerWithFe
15
15
  error: string;
16
16
  ok?: undefined;
17
17
  } | {
18
- error?: undefined;
19
18
  ok: boolean;
19
+ error?: undefined;
20
20
  }>>;
@@ -75,8 +75,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
75
75
  user: "user";
76
76
  }>>;
77
77
  }, z.core.$strip>>, {
78
- id?: undefined;
79
78
  deleted?: undefined;
79
+ id?: undefined;
80
80
  providers: {
81
81
  id: string;
82
82
  label: string;
@@ -93,9 +93,9 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
93
93
  label?: undefined;
94
94
  message?: undefined;
95
95
  } | {
96
- id?: undefined;
97
96
  deleted?: undefined;
98
97
  count?: undefined;
98
+ id?: undefined;
99
99
  providers?: undefined;
100
100
  found: boolean;
101
101
  provider: import("../custom-registry.js").CustomProviderConfig;
@@ -48,8 +48,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
48
48
  }>;
49
49
  /** DELETE /_agent-native/resources/:id — delete a resource */
50
50
  export declare function handleDeleteResource(event: any): Promise<{
51
- ok?: undefined;
52
51
  error: string;
52
+ ok?: undefined;
53
53
  } | {
54
54
  error?: undefined;
55
55
  ok: boolean;
@@ -34,16 +34,16 @@ export declare function createListSecretsHandler(): import("h3").EventHandlerWit
34
34
  /** POST /_agent-native/secrets/:key — write a secret. */
35
35
  export declare function createWriteSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
36
36
  error: string;
37
- ok?: undefined;
38
37
  status?: undefined;
38
+ ok?: undefined;
39
39
  } | {
40
40
  ok: boolean;
41
41
  status: string;
42
42
  error?: undefined;
43
43
  } | {
44
- ok?: undefined;
45
44
  error: string;
46
45
  removed?: undefined;
46
+ ok?: undefined;
47
47
  } | {
48
48
  ok: boolean;
49
49
  removed: boolean;
@@ -54,9 +54,9 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
54
54
  * or the current stored value without changing anything.
55
55
  */
56
56
  export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
57
- ok?: undefined;
58
57
  error: string;
59
58
  note?: undefined;
59
+ ok?: undefined;
60
60
  } | {
61
61
  ok: boolean;
62
62
  note?: undefined;
@@ -7,7 +7,7 @@ import { readBody } from "../server/h3-helpers.js";
7
7
  import { EMBED_TARGET_HEADER } from "../shared/embed-auth.js";
8
8
  import { isMcpEmbedCorsOrigin, MCP_EMBED_CORS_ALLOW_HEADERS, shouldAllowMcpEmbedCredentials, } from "../shared/mcp-embed-headers.js";
9
9
  import { actionCallIsReadOnly, notifyActionChange } from "./action-change.js";
10
- import { seedAgentRunOwnerContext, } from "./agent-run-context.js";
10
+ import { readBrowserSessionIdHeader, seedAgentRunOwnerContext, } from "./agent-run-context.js";
11
11
  import { captureError } from "./capture-error.js";
12
12
  import { getAllowedCorsOrigin as resolveAllowedCorsOrigin, readCorsAllowedOrigins, } from "./cors-origins.js";
13
13
  import { resolveEmbedSessionFromRequest, resolvedEmbedCapabilityScope, } from "./embed-session.js";
@@ -158,7 +158,7 @@ function handleOptionsRequest(event) {
158
158
  }
159
159
  setResponseHeader(event, "Access-Control-Allow-Methods", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS");
160
160
  setResponseHeader(event, "Access-Control-Allow-Headers", cors.credentials
161
- ? `Content-Type,Authorization,X-Requested-With,X-Request-Source,X-Agent-Native-CSRF,X-User-Timezone,X-Agent-Native-Tool-Bridge,X-Agent-Native-Tool-Id,X-Agent-Native-Frontend,X-Agent-Native-Client-Compatibility,X-Agent-Native-Build-Id,${EMBED_TARGET_HEADER}`
161
+ ? `Content-Type,Authorization,X-Requested-With,X-Request-Source,X-Agent-Native-CSRF,X-User-Timezone,X-Agent-Native-Session-Id,X-Agent-Native-Tool-Bridge,X-Agent-Native-Tool-Id,X-Agent-Native-Frontend,X-Agent-Native-Client-Compatibility,X-Agent-Native-Build-Id,${EMBED_TARGET_HEADER}`
162
162
  : `${MCP_EMBED_CORS_ALLOW_HEADERS},X-Agent-Native-Tool-Bridge,X-Agent-Native-Tool-Id,X-Agent-Native-Frontend,X-Agent-Native-Client-Compatibility,X-Agent-Native-Build-Id`);
163
163
  }
164
164
  setResponseStatus(event, 204);
@@ -350,12 +350,14 @@ export function mountActionRoutes(nitroApp, actions, options) {
350
350
  : undefined;
351
351
  }
352
352
  const timezone = readTimezoneHeader(event);
353
+ const browserSessionId = readBrowserSessionIdHeader(event);
353
354
  return runWithRequestContext({
354
355
  userEmail,
355
356
  userName,
356
357
  orgId,
357
358
  authCapability,
358
359
  timezone,
360
+ browserSessionId,
359
361
  requestOrigin: getRequestURL(event).origin,
360
362
  // Captured here because this is the last layer that still holds
361
363
  // the h3 event; everything below reads it off the request store.
@@ -27,10 +27,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
27
27
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
28
28
  error: any;
29
29
  } | {
30
- error?: undefined;
31
30
  ok: boolean;
32
31
  key: string;
33
32
  baseUrlKey?: string;
34
33
  scope: AgentEngineApiKeyScope;
34
+ error?: undefined;
35
35
  }>>;
36
36
  export {};
@@ -14,9 +14,11 @@ export declare function readAgentRunTimezone(event: H3Event): string | undefined
14
14
  *
15
15
  * Emitted as PostHog's `$session_id` on the run's `$ai_*` events so an agent
16
16
  * trace joins to the session replay it happened in. Distinct from
17
- * `$ai_session_id`, which is the conversation thread.
17
+ * `$ai_session_id`, which is the conversation thread. Read by both the agent
18
+ * run path and the HTTP action route, so one visit correlates across the UI's
19
+ * action calls and the agent's.
18
20
  */
19
- export declare function readAgentRunBrowserSessionId(event: H3Event): string | undefined;
21
+ export declare function readBrowserSessionIdHeader(event: H3Event): string | undefined;
20
22
  export declare function seedAgentRunOwnerContext(event: H3Event, ownerContext: AgentRunOwnerContext): AgentRunOwnerContext;
21
23
  export declare function seedBackgroundAgentRunOwnerContext(event: H3Event, runId: string): Promise<AgentRunOwnerContext | null>;
22
24
  export declare function resolveAgentRunOwnerContext(event: H3Event, options?: {
@@ -49,9 +49,11 @@ export function readAgentRunTimezone(event) {
49
49
  *
50
50
  * Emitted as PostHog's `$session_id` on the run's `$ai_*` events so an agent
51
51
  * trace joins to the session replay it happened in. Distinct from
52
- * `$ai_session_id`, which is the conversation thread.
52
+ * `$ai_session_id`, which is the conversation thread. Read by both the agent
53
+ * run path and the HTTP action route, so one visit correlates across the UI's
54
+ * action calls and the agent's.
53
55
  */
54
- export function readAgentRunBrowserSessionId(event) {
56
+ export function readBrowserSessionIdHeader(event) {
55
57
  const raw = readHeaderValue(event, "x-agent-native-session-id");
56
58
  const value = Array.isArray(raw) ? raw[0] : raw;
57
59
  return typeof value === "string" &&
@@ -140,7 +142,7 @@ export async function resolveAgentRunOrgId(options) {
140
142
  export async function resolveAgentRunRequestContext(options) {
141
143
  const orgId = await resolveAgentRunOrgId(options);
142
144
  const timezone = readAgentRunTimezone(options.event);
143
- const browserSessionId = readAgentRunBrowserSessionId(options.event);
145
+ const browserSessionId = readBrowserSessionIdHeader(options.event);
144
146
  const waitUntil = requestWaitUntil(options.event);
145
147
  const run = {
146
148
  ...(options.isBackgroundWorker ? { isBackgroundWorker: true } : {}),
@@ -33,6 +33,7 @@ import { registerBuiltinProviders } from "../tracking/providers.js";
33
33
  import { validateTrackPayload } from "../tracking/route.js";
34
34
  import { createAutomationsHandler } from "../triggers/routes.js";
35
35
  import { createAgentEngineApiKeyHandler } from "./agent-engine-api-key-route.js";
36
+ import { readBrowserSessionIdHeader } from "./agent-run-context.js";
36
37
  import { getConfiguredAppBasePath, stripAppBasePath } from "./app-base-path.js";
37
38
  import { getAppName } from "./app-name.js";
38
39
  import { getSession } from "./auth.js";
@@ -2544,6 +2545,7 @@ export function createCoreRoutesPlugin(options = {}) {
2544
2545
  try {
2545
2546
  track(validation.name, properties, {
2546
2547
  userId: userEmail,
2548
+ sessionId: readBrowserSessionIdHeader(event),
2547
2549
  });
2548
2550
  }
2549
2551
  catch {
@@ -26,8 +26,8 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
26
26
  expiresAt?: undefined;
27
27
  ttlSeconds?: undefined;
28
28
  } | {
29
- error?: undefined;
30
29
  token: string;
31
30
  expiresAt: string;
32
31
  ttlSeconds: number;
32
+ error?: undefined;
33
33
  }>>;
@@ -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";
@@ -134,6 +134,7 @@ function createPostHogProvider(apiKey, host, errorTracking) {
134
134
  properties: {
135
135
  distinct_id: distinctId,
136
136
  ...properties,
137
+ ...(event.sessionId ? { $session_id: event.sessionId } : {}),
137
138
  timestamp: event.timestamp,
138
139
  },
139
140
  }));
@@ -168,6 +169,7 @@ function createPostHogProvider(apiKey, host, errorTracking) {
168
169
  distinct_id: distinctId,
169
170
  properties: {
170
171
  ...event.properties,
172
+ ...(event.sessionId ? { $session_id: event.sessionId } : {}),
171
173
  timestamp: event.timestamp,
172
174
  },
173
175
  }));
@@ -225,6 +227,9 @@ function createMixpanelProvider(token) {
225
227
  ? new Date(event.timestamp).getTime() / 1000
226
228
  : undefined,
227
229
  ...event.properties,
230
+ // Mixpanel's own `$session_id` is numeric and assigned by its SDK, so
231
+ // the browser session lands as a plain property here.
232
+ ...(event.sessionId ? { session_id: event.sessionId } : {}),
228
233
  },
229
234
  };
230
235
  enqueue("https://api.mixpanel.com/track", JSON.stringify([data]));
@@ -253,7 +258,11 @@ function createAmplitudeProvider(apiKey) {
253
258
  {
254
259
  event_type: event.name,
255
260
  user_id: event.userId || "anonymous",
256
- event_properties: event.properties,
261
+ // Amplitude's top-level `session_id` must be a numeric epoch, so
262
+ // the browser session ships as an event property instead.
263
+ event_properties: event.sessionId
264
+ ? { ...event.properties, session_id: event.sessionId }
265
+ : event.properties,
257
266
  time: event.timestamp
258
267
  ? new Date(event.timestamp).getTime()
259
268
  : undefined,
@@ -290,6 +299,7 @@ function createWebhookProvider(url, authHeader) {
290
299
  event: event.name,
291
300
  properties: event.properties,
292
301
  userId: event.userId,
302
+ sessionId: event.sessionId,
293
303
  timestamp: event.timestamp,
294
304
  }), extra);
295
305
  },
@@ -318,6 +328,7 @@ function createAgentNativeAnalyticsProvider(publicKey, endpoint) {
318
328
  properties: event.properties ?? {},
319
329
  userId: event.userId,
320
330
  anonymousId: event.anonymousId,
331
+ sessionId: event.sessionId,
321
332
  timestamp: event.timestamp,
322
333
  }), undefined, { flushImmediately });
323
334
  },
@@ -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.2",
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": {
@@ -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.