@agentuity/frontend 0.0.110 → 0.0.112

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 (88) hide show
  1. package/dist/analytics/beacon.d.ts +15 -0
  2. package/dist/analytics/beacon.d.ts.map +1 -0
  3. package/dist/analytics/beacon.js +177 -0
  4. package/dist/analytics/beacon.js.map +1 -0
  5. package/dist/analytics/collectors/clicks.d.ts +10 -0
  6. package/dist/analytics/collectors/clicks.d.ts.map +1 -0
  7. package/dist/analytics/collectors/clicks.js +84 -0
  8. package/dist/analytics/collectors/clicks.js.map +1 -0
  9. package/dist/analytics/collectors/errors.d.ts +5 -0
  10. package/dist/analytics/collectors/errors.d.ts.map +1 -0
  11. package/dist/analytics/collectors/errors.js +43 -0
  12. package/dist/analytics/collectors/errors.js.map +1 -0
  13. package/dist/analytics/collectors/forms.d.ts +5 -0
  14. package/dist/analytics/collectors/forms.d.ts.map +1 -0
  15. package/dist/analytics/collectors/forms.js +55 -0
  16. package/dist/analytics/collectors/forms.js.map +1 -0
  17. package/dist/analytics/collectors/pageview.d.ts +15 -0
  18. package/dist/analytics/collectors/pageview.d.ts.map +1 -0
  19. package/dist/analytics/collectors/pageview.js +64 -0
  20. package/dist/analytics/collectors/pageview.js.map +1 -0
  21. package/dist/analytics/collectors/scroll.d.ts +17 -0
  22. package/dist/analytics/collectors/scroll.d.ts.map +1 -0
  23. package/dist/analytics/collectors/scroll.js +93 -0
  24. package/dist/analytics/collectors/scroll.js.map +1 -0
  25. package/dist/analytics/collectors/spa.d.ts +10 -0
  26. package/dist/analytics/collectors/spa.d.ts.map +1 -0
  27. package/dist/analytics/collectors/spa.js +53 -0
  28. package/dist/analytics/collectors/spa.js.map +1 -0
  29. package/dist/analytics/collectors/visibility.d.ts +18 -0
  30. package/dist/analytics/collectors/visibility.d.ts.map +1 -0
  31. package/dist/analytics/collectors/visibility.js +81 -0
  32. package/dist/analytics/collectors/visibility.js.map +1 -0
  33. package/dist/analytics/collectors/webvitals.d.ts +6 -0
  34. package/dist/analytics/collectors/webvitals.d.ts.map +1 -0
  35. package/dist/analytics/collectors/webvitals.js +111 -0
  36. package/dist/analytics/collectors/webvitals.js.map +1 -0
  37. package/dist/analytics/events.d.ts +18 -0
  38. package/dist/analytics/events.d.ts.map +1 -0
  39. package/dist/analytics/events.js +126 -0
  40. package/dist/analytics/events.js.map +1 -0
  41. package/dist/analytics/index.d.ts +12 -0
  42. package/dist/analytics/index.d.ts.map +1 -0
  43. package/dist/analytics/index.js +12 -0
  44. package/dist/analytics/index.js.map +1 -0
  45. package/dist/analytics/offline.d.ts +19 -0
  46. package/dist/analytics/offline.d.ts.map +1 -0
  47. package/dist/analytics/offline.js +145 -0
  48. package/dist/analytics/offline.js.map +1 -0
  49. package/dist/analytics/types.d.ts +113 -0
  50. package/dist/analytics/types.d.ts.map +1 -0
  51. package/dist/analytics/types.js +2 -0
  52. package/dist/analytics/types.js.map +1 -0
  53. package/dist/analytics/utils/storage.d.ts +13 -0
  54. package/dist/analytics/utils/storage.d.ts.map +1 -0
  55. package/dist/analytics/utils/storage.js +63 -0
  56. package/dist/analytics/utils/storage.js.map +1 -0
  57. package/dist/analytics/utils/utm.d.ts +12 -0
  58. package/dist/analytics/utils/utm.d.ts.map +1 -0
  59. package/dist/analytics/utils/utm.js +27 -0
  60. package/dist/analytics/utils/utm.js.map +1 -0
  61. package/dist/client/index.d.ts.map +1 -1
  62. package/dist/client/index.js +93 -15
  63. package/dist/client/index.js.map +1 -1
  64. package/dist/client/types.d.ts +51 -5
  65. package/dist/client/types.d.ts.map +1 -1
  66. package/dist/index.d.ts +1 -0
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +2 -0
  69. package/dist/index.js.map +1 -1
  70. package/package.json +3 -3
  71. package/src/analytics/beacon.ts +203 -0
  72. package/src/analytics/collectors/clicks.ts +100 -0
  73. package/src/analytics/collectors/errors.ts +49 -0
  74. package/src/analytics/collectors/forms.ts +64 -0
  75. package/src/analytics/collectors/pageview.ts +76 -0
  76. package/src/analytics/collectors/scroll.ts +112 -0
  77. package/src/analytics/collectors/spa.ts +60 -0
  78. package/src/analytics/collectors/visibility.ts +94 -0
  79. package/src/analytics/collectors/webvitals.ts +129 -0
  80. package/src/analytics/events.ts +144 -0
  81. package/src/analytics/index.ts +21 -0
  82. package/src/analytics/offline.ts +163 -0
  83. package/src/analytics/types.ts +139 -0
  84. package/src/analytics/utils/storage.ts +64 -0
  85. package/src/analytics/utils/utm.ts +36 -0
  86. package/src/client/index.ts +109 -15
  87. package/src/client/types.ts +104 -17
  88. package/src/index.ts +18 -0
@@ -0,0 +1,163 @@
1
+ import type { AnalyticsEvent } from './types';
2
+
3
+ const DB_NAME = 'agentuity_analytics';
4
+ const STORE_NAME = 'events';
5
+ const DB_VERSION = 1;
6
+ const MAX_QUEUE_SIZE = 1000;
7
+
8
+ let db: IDBDatabase | null = null;
9
+ let dbInitPromise: Promise<IDBDatabase | null> | null = null;
10
+
11
+ /**
12
+ * Initialize IndexedDB for offline event storage
13
+ */
14
+ async function initDB(): Promise<IDBDatabase | null> {
15
+ if (typeof indexedDB === 'undefined') {
16
+ return null;
17
+ }
18
+
19
+ return new Promise((resolve) => {
20
+ try {
21
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
22
+
23
+ request.onerror = () => {
24
+ resolve(null);
25
+ };
26
+
27
+ request.onsuccess = () => {
28
+ resolve(request.result);
29
+ };
30
+
31
+ request.onupgradeneeded = (e) => {
32
+ const database = (e.target as IDBOpenDBRequest).result;
33
+ if (!database.objectStoreNames.contains(STORE_NAME)) {
34
+ database.createObjectStore(STORE_NAME, { keyPath: 'id' });
35
+ }
36
+ };
37
+ } catch {
38
+ resolve(null);
39
+ }
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Get database instance
45
+ */
46
+ async function getDB(): Promise<IDBDatabase | null> {
47
+ if (db) {
48
+ return db;
49
+ }
50
+
51
+ if (!dbInitPromise) {
52
+ dbInitPromise = initDB();
53
+ }
54
+
55
+ db = await dbInitPromise;
56
+ return db;
57
+ }
58
+
59
+ /**
60
+ * Store event in IndexedDB for offline persistence
61
+ */
62
+ export async function storeOfflineEvent(event: AnalyticsEvent): Promise<void> {
63
+ const database = await getDB();
64
+ if (!database) {
65
+ return;
66
+ }
67
+
68
+ try {
69
+ const transaction = database.transaction(STORE_NAME, 'readwrite');
70
+ const store = transaction.objectStore(STORE_NAME);
71
+
72
+ // Check current count and evict old events if needed before adding
73
+ const count = await new Promise<number>((resolve) => {
74
+ const countRequest = store.count();
75
+ countRequest.onsuccess = () => resolve(countRequest.result);
76
+ countRequest.onerror = () => resolve(0);
77
+ });
78
+
79
+ if (count >= MAX_QUEUE_SIZE) {
80
+ // Evict oldest event (FIFO) before adding new one
81
+ await new Promise<void>((resolve) => {
82
+ const cursorRequest = store.openCursor();
83
+ cursorRequest.onsuccess = () => {
84
+ const cursor = cursorRequest.result;
85
+ if (cursor) {
86
+ const deleteRequest = cursor.delete();
87
+ deleteRequest.onsuccess = () => resolve();
88
+ deleteRequest.onerror = () => resolve();
89
+ } else {
90
+ resolve();
91
+ }
92
+ };
93
+ cursorRequest.onerror = () => resolve();
94
+ });
95
+ }
96
+
97
+ store.add(event);
98
+ } catch {
99
+ // Silent failure
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Get all offline events and clear them
105
+ */
106
+ export async function getAndClearOfflineEvents(): Promise<AnalyticsEvent[]> {
107
+ const database = await getDB();
108
+ if (!database) {
109
+ return [];
110
+ }
111
+
112
+ return new Promise((resolve) => {
113
+ try {
114
+ const transaction = database.transaction(STORE_NAME, 'readwrite');
115
+ const store = transaction.objectStore(STORE_NAME);
116
+
117
+ const events: AnalyticsEvent[] = [];
118
+ const request = store.openCursor();
119
+
120
+ request.onsuccess = () => {
121
+ const cursor = request.result;
122
+ if (cursor) {
123
+ events.push(cursor.value as AnalyticsEvent);
124
+ cursor.delete();
125
+ cursor.continue();
126
+ } else {
127
+ resolve(events);
128
+ }
129
+ };
130
+
131
+ request.onerror = () => {
132
+ resolve([]);
133
+ };
134
+ } catch {
135
+ resolve([]);
136
+ }
137
+ });
138
+ }
139
+
140
+ /**
141
+ * Check if we're online
142
+ */
143
+ export function isOnline(): boolean {
144
+ if (typeof navigator === 'undefined') {
145
+ return true;
146
+ }
147
+ return navigator.onLine !== false;
148
+ }
149
+
150
+ /**
151
+ * Initialize offline support
152
+ * Listens for online event to flush queued events
153
+ */
154
+ export function initOfflineSupport(flushCallback: () => void): void {
155
+ if (typeof window === 'undefined') {
156
+ return;
157
+ }
158
+
159
+ window.addEventListener('online', () => {
160
+ // Flush offline events when coming back online
161
+ flushCallback();
162
+ });
163
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Analytics event types
3
+ */
4
+ export type AnalyticsEventType =
5
+ | 'pageview'
6
+ | 'click'
7
+ | 'scroll'
8
+ | 'visibility'
9
+ | 'error'
10
+ | 'custom'
11
+ | 'web_vital'
12
+ | 'form_submit'
13
+ | 'outbound_link';
14
+
15
+ /**
16
+ * Analytics event sent to the collection endpoint
17
+ */
18
+ export interface AnalyticsEvent {
19
+ id: string;
20
+ timestamp: number;
21
+ timezone_offset: number;
22
+
23
+ event_type: AnalyticsEventType;
24
+ event_name?: string;
25
+ event_data?: Record<string, unknown>;
26
+
27
+ url: string;
28
+ path: string;
29
+ referrer: string;
30
+ title: string;
31
+
32
+ screen_width: number;
33
+ screen_height: number;
34
+ viewport_width: number;
35
+ viewport_height: number;
36
+ device_pixel_ratio: number;
37
+ user_agent: string;
38
+ language: string;
39
+
40
+ load_time?: number;
41
+ dom_ready?: number;
42
+ ttfb?: number;
43
+ fcp?: number;
44
+ lcp?: number;
45
+ cls?: number;
46
+ inp?: number;
47
+
48
+ scroll_depth?: number;
49
+ time_on_page?: number;
50
+
51
+ utm_source?: string;
52
+ utm_medium?: string;
53
+ utm_campaign?: string;
54
+ utm_term?: string;
55
+ utm_content?: string;
56
+ }
57
+
58
+ /**
59
+ * Batch payload sent to /_agentuity/webanalytics/collect
60
+ */
61
+ export interface AnalyticsBatchPayload {
62
+ org_id: string;
63
+ project_id: string;
64
+ session_id: string;
65
+ thread_id: string;
66
+ visitor_id: string;
67
+ is_devmode: boolean;
68
+ events: AnalyticsEvent[];
69
+ }
70
+
71
+ /**
72
+ * Configuration injected by SDK runtime into window.__AGENTUITY_ANALYTICS__
73
+ */
74
+ export interface AnalyticsPageConfig {
75
+ enabled: boolean;
76
+ orgId: string;
77
+ projectId: string;
78
+ sessionId: string;
79
+ threadId: string;
80
+ isDevmode: boolean;
81
+
82
+ trackClicks?: boolean;
83
+ trackScroll?: boolean;
84
+ trackOutboundLinks?: boolean;
85
+ trackForms?: boolean;
86
+ trackWebVitals?: boolean;
87
+ trackErrors?: boolean;
88
+ trackSPANavigation?: boolean;
89
+ requireConsent?: boolean;
90
+ sampleRate?: number;
91
+ excludePatterns?: string[];
92
+ globalProperties?: Record<string, unknown>;
93
+ }
94
+
95
+ /**
96
+ * Public analytics client interface
97
+ */
98
+ export interface AnalyticsClient {
99
+ /**
100
+ * Track a custom event
101
+ */
102
+ track(eventName: string, properties?: Record<string, unknown>): void;
103
+
104
+ /**
105
+ * Identify the current user (sets visitor properties)
106
+ */
107
+ identify(userId: string, traits?: Record<string, unknown>): void;
108
+
109
+ /**
110
+ * Manually track a page view
111
+ */
112
+ pageview(path?: string): void;
113
+
114
+ /**
115
+ * Flush pending events immediately
116
+ */
117
+ flush(): Promise<void>;
118
+
119
+ /**
120
+ * Opt out of analytics
121
+ */
122
+ optOut(): void;
123
+
124
+ /**
125
+ * Opt back in to analytics
126
+ */
127
+ optIn(): void;
128
+
129
+ /**
130
+ * Check if analytics is currently enabled
131
+ */
132
+ isEnabled(): boolean;
133
+ }
134
+
135
+ declare global {
136
+ interface Window {
137
+ __AGENTUITY_ANALYTICS__?: AnalyticsPageConfig;
138
+ }
139
+ }
@@ -0,0 +1,64 @@
1
+ const VISITOR_ID_KEY = 'agentuity_visitor_id';
2
+ const OPT_OUT_KEY = 'agentuity_analytics_optout';
3
+
4
+ /**
5
+ * Generate a random UUID v4
6
+ */
7
+ function generateUUID(): string {
8
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
9
+ return crypto.randomUUID();
10
+ }
11
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
12
+ const r = (Math.random() * 16) | 0;
13
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
14
+ return v.toString(16);
15
+ });
16
+ }
17
+
18
+ /**
19
+ * Get or create the visitor ID from localStorage
20
+ */
21
+ export function getVisitorId(): string {
22
+ if (typeof localStorage === 'undefined') {
23
+ return generateUUID();
24
+ }
25
+
26
+ let visitorId = localStorage.getItem(VISITOR_ID_KEY);
27
+ if (!visitorId) {
28
+ visitorId = `vid_${generateUUID()}`;
29
+ try {
30
+ localStorage.setItem(VISITOR_ID_KEY, visitorId);
31
+ } catch {
32
+ // localStorage might be full or disabled
33
+ }
34
+ }
35
+ return visitorId;
36
+ }
37
+
38
+ /**
39
+ * Check if user has opted out
40
+ */
41
+ export function isOptedOut(): boolean {
42
+ if (typeof localStorage === 'undefined') {
43
+ return false;
44
+ }
45
+ return localStorage.getItem(OPT_OUT_KEY) === 'true';
46
+ }
47
+
48
+ /**
49
+ * Set opt-out status
50
+ */
51
+ export function setOptOut(optOut: boolean): void {
52
+ if (typeof localStorage === 'undefined') {
53
+ return;
54
+ }
55
+ try {
56
+ if (optOut) {
57
+ localStorage.setItem(OPT_OUT_KEY, 'true');
58
+ } else {
59
+ localStorage.removeItem(OPT_OUT_KEY);
60
+ }
61
+ } catch {
62
+ // localStorage might be full or disabled
63
+ }
64
+ }
@@ -0,0 +1,36 @@
1
+ export interface UTMParams {
2
+ utm_source?: string;
3
+ utm_medium?: string;
4
+ utm_campaign?: string;
5
+ utm_term?: string;
6
+ utm_content?: string;
7
+ }
8
+
9
+ /**
10
+ * Extract UTM parameters from the current URL
11
+ */
12
+ export function getUTMParams(): UTMParams {
13
+ if (typeof window === 'undefined') {
14
+ return {};
15
+ }
16
+
17
+ const params = new URLSearchParams(window.location.search);
18
+ const utm: UTMParams = {};
19
+
20
+ const source = params.get('utm_source');
21
+ if (source) utm.utm_source = source;
22
+
23
+ const medium = params.get('utm_medium');
24
+ if (medium) utm.utm_medium = medium;
25
+
26
+ const campaign = params.get('utm_campaign');
27
+ if (campaign) utm.utm_campaign = campaign;
28
+
29
+ const term = params.get('utm_term');
30
+ if (term) utm.utm_term = term;
31
+
32
+ const content = params.get('utm_content');
33
+ if (content) utm.utm_content = content;
34
+
35
+ return utm;
36
+ }
@@ -24,6 +24,40 @@ function resolveHeaders(
24
24
  return typeof headers === 'function' ? headers() : headers;
25
25
  }
26
26
 
27
+ /**
28
+ * Escape special regex characters in a string.
29
+ */
30
+ function escapeRegExp(str: string): string {
31
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
32
+ }
33
+
34
+ /**
35
+ * Substitute path parameters in a URL path template.
36
+ * E.g., '/api/users/:id' with { id: '123' } becomes '/api/users/123'
37
+ */
38
+ function substitutePathParams(pathTemplate: string, pathParams?: Record<string, string>): string {
39
+ if (!pathParams) return pathTemplate;
40
+
41
+ let result = pathTemplate;
42
+ for (const [key, value] of Object.entries(pathParams)) {
43
+ const escapedKey = escapeRegExp(key);
44
+ result = result.replace(new RegExp(`:${escapedKey}\\??`, 'g'), encodeURIComponent(value));
45
+ result = result.replace(new RegExp(`\\*${escapedKey}`, 'g'), encodeURIComponent(value));
46
+ }
47
+ return result;
48
+ }
49
+
50
+ /**
51
+ * Build URL with query params.
52
+ */
53
+ function buildUrlWithQuery(baseUrl: string, path: string, query?: Record<string, string>): string {
54
+ const url = `${baseUrl}${path}`;
55
+ if (!query || Object.keys(query).length === 0) return url;
56
+
57
+ const params = new URLSearchParams(query);
58
+ return `${url}?${params.toString()}`;
59
+ }
60
+
27
61
  /**
28
62
  * Create a type-safe API client from a RouteRegistry.
29
63
  *
@@ -76,37 +110,97 @@ export function createClient<R>(options: ClientOptions = {}, metadata?: unknown)
76
110
  if (isTerminalMethod && currentPath.length >= 1) {
77
111
  const method = prop;
78
112
  const pathSegments = currentPath;
79
- const urlPath = '/api/' + pathSegments.join('/');
80
113
 
81
- // Determine route type
114
+ // Look up route metadata
82
115
  let routeType = 'api';
116
+ let routePath: string | undefined;
117
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
118
+ let metaNode: any = metadata;
119
+
83
120
  if (isStreamMethod) {
84
121
  // Stream methods directly specify the route type
85
122
  if (method === 'websocket') routeType = 'websocket';
86
123
  else if (method === 'eventstream') routeType = 'sse';
87
124
  else if (method === 'stream') routeType = 'stream';
88
- } else if (metadata) {
89
- // Look up route type from metadata for HTTP methods
90
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
91
- let metaNode: any = metadata;
125
+ }
126
+
127
+ if (metadata) {
92
128
  for (const segment of pathSegments) {
93
129
  if (metaNode && typeof metaNode === 'object') {
94
130
  metaNode = metaNode[segment];
95
131
  }
96
132
  }
97
- if (metaNode && typeof metaNode === 'object' && metaNode[method]?.type) {
98
- routeType = metaNode[method].type;
133
+ if (metaNode && typeof metaNode === 'object' && metaNode[method]) {
134
+ if (metaNode[method].type) {
135
+ routeType = metaNode[method].type;
136
+ }
137
+ if (metaNode[method].path) {
138
+ routePath = metaNode[method].path;
139
+ }
99
140
  }
100
141
  }
101
142
 
102
- return (input?: unknown) => {
143
+ // Fallback URL path if no metadata
144
+ const fallbackPath = '/api/' + pathSegments.join('/');
145
+
146
+ return (...args: unknown[]) => {
103
147
  const resolvedBaseUrl = resolveBaseUrl(baseUrl);
104
148
  const resolvedHeaders = resolveHeaders(defaultHeaders);
105
149
 
150
+ // Get path param names from metadata if available
151
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
152
+ const pathParamNames: string[] | undefined = (metaNode as any)?.[method]?.pathParams;
153
+ const hasPathParams = pathParamNames && pathParamNames.length > 0;
154
+
155
+ let pathParams: Record<string, string> | undefined;
156
+ let input: unknown;
157
+ let query: Record<string, string> | undefined;
158
+
159
+ if (hasPathParams) {
160
+ // Route has path params - positional arguments API
161
+ // Args are: param1, param2, ..., [options?]
162
+ // Example: client.user.get('123', 12) or client.user.get('123', 12, { query: {...} })
163
+ pathParams = {};
164
+
165
+ for (let i = 0; i < pathParamNames.length; i++) {
166
+ const arg = args[i];
167
+ if (arg === undefined || arg === null) {
168
+ throw new Error(
169
+ `Missing required path parameter '${pathParamNames[i]}' at position ${i + 1}. ` +
170
+ `Expected ${pathParamNames.length} path parameter(s): ${pathParamNames.join(', ')}`
171
+ );
172
+ }
173
+ pathParams[pathParamNames[i]] = String(arg);
174
+ }
175
+
176
+ // Check if there's an options object after the path params
177
+ const optionsArg = args[pathParamNames.length];
178
+ if (optionsArg && typeof optionsArg === 'object') {
179
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
180
+ const opts = optionsArg as any;
181
+ input = opts.input;
182
+ query = opts.query;
183
+ }
184
+ } else {
185
+ // No path params - use existing behavior
186
+ const options = args[0];
187
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
188
+ const opts = options as any;
189
+ const isOptionsObject =
190
+ opts && typeof opts === 'object' && ('input' in opts || 'query' in opts);
191
+
192
+ input = isOptionsObject ? opts.input : options;
193
+ query = isOptionsObject ? opts.query : undefined;
194
+ }
195
+
196
+ // Substitute path params in the route path
197
+ const basePath = routePath || fallbackPath;
198
+ const urlPath = substitutePathParams(basePath, pathParams);
199
+
106
200
  // WebSocket endpoint
107
201
  if (routeType === 'websocket') {
108
202
  const wsBaseUrl = resolvedBaseUrl.replace(/^http/, 'ws');
109
- const wsUrl = `${wsBaseUrl}${urlPath}`;
203
+ const wsUrl = buildUrlWithQuery(wsBaseUrl, urlPath, query);
110
204
  const ws = createWebSocketClient(wsUrl);
111
205
  if (input) {
112
206
  ws.on('open', () => ws.send(input));
@@ -116,9 +210,7 @@ export function createClient<R>(options: ClientOptions = {}, metadata?: unknown)
116
210
 
117
211
  // SSE endpoint
118
212
  if (routeType === 'sse') {
119
- const sseUrl = `${resolvedBaseUrl}${urlPath}`;
120
- // Note: Native EventSource doesn't support custom headers
121
- // For auth, use withCredentials or consider fetch-based alternatives like @microsoft/fetch-event-source
213
+ const sseUrl = buildUrlWithQuery(resolvedBaseUrl, urlPath, query);
122
214
  return createEventStreamClient(sseUrl, {
123
215
  withCredentials: Object.keys(resolvedHeaders).length > 0,
124
216
  });
@@ -126,7 +218,8 @@ export function createClient<R>(options: ClientOptions = {}, metadata?: unknown)
126
218
 
127
219
  // Stream endpoint
128
220
  if (routeType === 'stream') {
129
- return fetch(`${resolvedBaseUrl}${urlPath}`, {
221
+ const streamUrl = buildUrlWithQuery(resolvedBaseUrl, urlPath, query);
222
+ return fetch(streamUrl, {
130
223
  method: method.toUpperCase(),
131
224
  headers: { 'Content-Type': contentType, ...resolvedHeaders },
132
225
  body: input ? JSON.stringify(input) : undefined,
@@ -138,7 +231,8 @@ export function createClient<R>(options: ClientOptions = {}, metadata?: unknown)
138
231
  }
139
232
 
140
233
  // Regular API endpoint
141
- return fetch(`${resolvedBaseUrl}${urlPath}`, {
234
+ const apiUrl = buildUrlWithQuery(resolvedBaseUrl, urlPath, query);
235
+ return fetch(apiUrl, {
142
236
  method: method.toUpperCase(),
143
237
  headers: { 'Content-Type': contentType, ...resolvedHeaders },
144
238
  body: method.toUpperCase() !== 'GET' && input ? JSON.stringify(input) : undefined,