@listo-ai/mcp-observability 0.2.0 → 0.3.0

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.
package/README.md CHANGED
@@ -4,13 +4,14 @@ Lightweight telemetry SDK for MCP servers and web applications. Captures HTTP re
4
4
 
5
5
  ## Overview
6
6
 
7
- This SDK provides comprehensive observability for [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) servers and Express.js applications. It integrates with Listo Insights for centralized analytics, and includes a local development dashboard for real-time metrics.
7
+ This SDK provides comprehensive observability for [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) servers and Express.js applications. It supports centralized analytics via a remote sink, and includes a local development dashboard for real-time metrics.
8
8
 
9
9
  **Key features:**
10
10
 
11
11
  - **Easy setup** -- Get started in 4 lines of code with `createMcpObservabilityEasy()`
12
12
  - **Local dashboard** -- Real-time metrics dashboard during development at `/telemetry/dashboard`
13
- - **Centralized analytics** -- Send telemetry data to Listo Insights in production
13
+ - **Browser client** -- Reusable `TelemetryClient` for browser apps with batching and `sendBeacon` fallback
14
+ - **Centralized analytics** -- Send telemetry data to a remote endpoint in production
14
15
  - **Automatic tracking** -- HTTP requests via Express middleware, MCP tool invocations via handler wrapper
15
16
  - **Payload sanitization** -- Built-in redaction of sensitive keys (`password`, `token`, `apiKey`, `secret`, `authorization`)
16
17
  - **Sampling** -- Configurable sampling rates with guaranteed capture of errors and session events
@@ -20,6 +21,7 @@ This SDK provides comprehensive observability for [Model Context Protocol (MCP)]
20
21
 
21
22
  - [Installation](#installation)
22
23
  - [Quick Start](#quick-start)
24
+ - [Browser Client](#browser-client)
23
25
  - [How It Works](#how-it-works)
24
26
  - [API Reference](#api-reference)
25
27
  - [Integration Guide](#integration-guide)
@@ -56,14 +58,14 @@ const observability = createMcpObservabilityEasy({
56
58
  ```typescript
57
59
  import { createTelemetryRouter } from '@listo-ai/mcp-observability';
58
60
 
59
- app.use('/telemetry', createTelemetryRouter());
61
+ app.use('/telemetry', createTelemetryRouter(express));
60
62
  ```
61
63
 
62
- ### 3. Set environment variables
64
+ ### 3. Set environment variables (optional, for remote telemetry)
63
65
 
64
66
  ```bash
65
- INSIGHTS_API_URL=https://api.listoai.co
66
- INSIGHTS_API_KEY=fuse_xxxxxxxxxxxxx
67
+ LISTO_API_URL=https://your-api.example.com
68
+ LISTO_API_KEY=your_api_key
67
69
  ```
68
70
 
69
71
  ### 4. Access the local dashboard
@@ -72,6 +74,23 @@ INSIGHTS_API_KEY=fuse_xxxxxxxxxxxxx
72
74
  http://localhost:3000/telemetry/dashboard
73
75
  ```
74
76
 
77
+ ## Browser Client
78
+
79
+ For browser applications, use the lightweight client module with automatic batching and session management:
80
+
81
+ ```typescript
82
+ import { createTelemetryClient } from '@listo-ai/mcp-observability/client';
83
+
84
+ const client = createTelemetryClient({
85
+ endpoint: 'https://myapp.com/telemetry',
86
+ });
87
+
88
+ client.track('page_view', { path: '/home' });
89
+ client.trackUi('button_click', { action: 'click', widgetId: 'cta-1' });
90
+ ```
91
+
92
+ The client has no Node.js dependencies — it uses `fetch()`, `crypto.randomUUID()`, and `navigator.sendBeacon()`. See [Browser Client docs](./docs/client.md) for full API reference.
93
+
75
94
  ## How It Works
76
95
 
77
96
  ### Environment-Based Auto-Configuration
@@ -97,7 +116,7 @@ Observability SDK (captures events)
97
116
  |
98
117
  +---> Console Sink (dev only, errors)
99
118
  +---> InMemory Sink (local dashboard)
100
- +---> Remote Sink (Listo Insights, batched)
119
+ +---> Remote Sink (remote endpoint, batched)
101
120
  ```
102
121
 
103
122
  ### Sink Architecture
@@ -105,7 +124,7 @@ Observability SDK (captures events)
105
124
  Events flow through configurable **sinks** -- decoupled consumers that process telemetry data:
106
125
 
107
126
  - **`InMemorySink`** -- Circular buffer (default 2000 events) powering the local dashboard
108
- - **`RemoteSink`** -- Batches events and sends them to Listo Insights with exponential backoff retries
127
+ - **`RemoteSink`** -- Batches events and sends them to a remote endpoint with exponential backoff retries
109
128
  - **`ConsoleSink`** -- Logs errors to stdout
110
129
  - **`combineSinks()`** -- Routes events to multiple sinks simultaneously
111
130
 
@@ -120,8 +139,8 @@ interface EasySetupConfig {
120
139
  serviceName: string;
121
140
  serviceVersion?: string; // default: "1.0.0"
122
141
  environment?: "dev" | "staging" | "production";
123
- insightsApiUrl?: string; // default: process.env.INSIGHTS_API_URL
124
- insightsApiKey?: string; // default: process.env.INSIGHTS_API_KEY
142
+ listoApiUrl?: string; // default: process.env.LISTO_API_URL
143
+ listoApiKey?: string; // default: process.env.LISTO_API_KEY
125
144
  enableLocalDashboard?: boolean; // default: true in dev, false in prod
126
145
  sampleRate?: number; // default: 1 in dev, 0.1 in prod
127
146
  }
@@ -129,7 +148,7 @@ interface EasySetupConfig {
129
148
 
130
149
  ```typescript
131
150
  const observability = createMcpObservabilityEasy({
132
- serviceName: 'hotel-search-mcp',
151
+ serviceName: 'my-mcp-server',
133
152
  serviceVersion: '0.1.0',
134
153
  enableLocalDashboard: true,
135
154
  });
@@ -152,7 +171,7 @@ interface ObservabilityOptions {
152
171
  }
153
172
  ```
154
173
 
155
- ### `createTelemetryRouter()`
174
+ ### `createTelemetryRouter(express)`
156
175
 
157
176
  Express router providing telemetry endpoints:
158
177
 
@@ -160,12 +179,13 @@ Express router providing telemetry endpoints:
160
179
  | --- | --- | --- |
161
180
  | `/` | GET | JSON metrics data |
162
181
  | `/dashboard` | GET | HTML dashboard with auto-refresh |
163
- | `/event` | POST | UI event ingestion |
182
+ | `/event` | POST | Single UI event ingestion |
183
+ | `/events` | POST | Batch UI event ingestion (max 100) |
164
184
 
165
185
  ```typescript
166
186
  import { createTelemetryRouter } from '@listo-ai/mcp-observability';
167
187
 
168
- app.use('/telemetry', createTelemetryRouter());
188
+ app.use('/telemetry', createTelemetryRouter(express));
169
189
  // Dashboard: http://localhost:3000/telemetry/dashboard
170
190
  ```
171
191
 
@@ -204,10 +224,10 @@ server.setRequestHandler(
204
224
  Record custom business events for domain-specific analytics.
205
225
 
206
226
  ```typescript
207
- observability.recordBusinessEvent('hotel_search', {
208
- q: 'paris hotels',
209
- minRating: 4.5,
210
- total: 42,
227
+ observability.recordBusinessEvent('product_search', {
228
+ q: 'premium items',
229
+ category: 'electronics',
230
+ results: 42,
211
231
  }, 'ok');
212
232
  ```
213
233
 
@@ -217,8 +237,10 @@ Record UI interaction events from the browser.
217
237
 
218
238
  ```typescript
219
239
  observability.recordUiEvent({
220
- name: 'booking_clicked',
221
- properties: { hotelId: 'h123', hotelName: 'Hotel Paris' },
240
+ name: 'cta_click',
241
+ action: 'click',
242
+ widgetId: 'signup-btn',
243
+ properties: { itemId: 'i-123' },
222
244
  sessionId: 'session-456',
223
245
  tenantId: 'tenant-789',
224
246
  });
@@ -244,8 +266,8 @@ Standalone remote sink for custom configurations.
244
266
  import { RemoteSink } from '@listo-ai/mcp-observability';
245
267
 
246
268
  const sink = new RemoteSink({
247
- endpoint: 'https://api.listoai.co/v1/events/batch',
248
- apiKey: 'fuse_xxxxxxxxxxxxx',
269
+ endpoint: 'https://your-api.example.com/v1/events/batch',
270
+ apiKey: 'your_api_key',
249
271
  batchSize: 50, // events per batch
250
272
  flushIntervalMs: 5000, // flush every 5s
251
273
  maxRetries: 3, // retry with exponential backoff
@@ -275,11 +297,11 @@ const observability = createMcpObservabilityEasy({
275
297
  app.use(expressTelemetry(observability));
276
298
 
277
299
  // Telemetry endpoints
278
- app.use('/telemetry', createTelemetryRouter());
300
+ app.use('/telemetry', createTelemetryRouter(express));
279
301
 
280
- app.get('/hotels', (req, res) => {
302
+ app.get('/products', (req, res) => {
281
303
  // Automatically tracked
282
- res.json({ hotels: [] });
304
+ res.json({ products: [] });
283
305
  });
284
306
 
285
307
  app.listen(3000);
@@ -297,14 +319,14 @@ import express from 'express';
297
319
 
298
320
  const app = express();
299
321
  const observability = createMcpObservabilityEasy({
300
- serviceName: 'hotel-search-mcp',
322
+ serviceName: 'my-mcp-server',
301
323
  serviceVersion: '0.1.0',
302
324
  });
303
325
 
304
- app.use('/telemetry', createTelemetryRouter());
326
+ app.use('/telemetry', createTelemetryRouter(express));
305
327
 
306
328
  const server = new Server({
307
- name: 'hotel-search',
329
+ name: 'my-mcp-server',
308
330
  version: '0.1.0',
309
331
  });
310
332
 
@@ -332,8 +354,8 @@ Automatically captured via `expressTelemetry()` middleware.
332
354
  {
333
355
  type: 'http_request',
334
356
  method: 'GET',
335
- path: '/hotels',
336
- route: '/hotels/:city',
357
+ path: '/products',
358
+ route: '/products/:id',
337
359
  statusCode: 200,
338
360
  status: 'ok',
339
361
  latencyMs: 45.2,
@@ -349,7 +371,7 @@ Captured via `wrapMcpHandler()`.
349
371
  {
350
372
  type: 'mcp_request',
351
373
  requestKind: 'CallTool',
352
- toolName: 'search_hotels',
374
+ toolName: 'search_products',
353
375
  status: 'ok',
354
376
  latencyMs: 234.5,
355
377
  inputBytes: 156,
@@ -376,9 +398,9 @@ Custom domain events via `recordBusinessEvent()`.
376
398
  ```typescript
377
399
  {
378
400
  type: 'business_event',
379
- name: 'hotel_search',
401
+ name: 'product_search',
380
402
  status: 'ok',
381
- properties: { q: 'paris', results: 42 },
403
+ properties: { q: 'premium', results: 42 },
382
404
  }
383
405
  ```
384
406
 
@@ -389,9 +411,10 @@ Browser interaction events via `recordUiEvent()` or the `POST /telemetry/event`
389
411
  ```typescript
390
412
  {
391
413
  type: 'ui_event',
392
- name: 'booking_clicked',
414
+ name: 'cta_click',
415
+ action: 'click',
393
416
  sessionId: 'session-123',
394
- properties: { hotelId: 'h456' },
417
+ properties: { itemId: 'i-456' },
395
418
  }
396
419
  ```
397
420
 
@@ -399,8 +422,8 @@ Browser interaction events via `recordUiEvent()` or the `POST /telemetry/event`
399
422
 
400
423
  | Variable | Required | Description |
401
424
  | --- | --- | --- |
402
- | `INSIGHTS_API_URL` | For remote | Listo Insights API endpoint (default: `https://api.listoai.co`) |
403
- | `INSIGHTS_API_KEY` | For remote | API key for authentication (format: `fuse_xxxxxxxxxxxxx`) |
425
+ | `LISTO_API_URL` | For remote | Listo API endpoint |
426
+ | `LISTO_API_KEY` | For remote | API key for authentication |
404
427
  | `NODE_ENV` | No | `development` / `staging` / `production` -- controls defaults |
405
428
  | `TELEMETRY_SAMPLE_RATE` | No | Override sampling percentage (0.0 - 1.0) |
406
429
  | `TELEMETRY_CAPTURE_PAYLOADS` | No | Enable/disable payload capture |
@@ -411,7 +434,7 @@ Browser interaction events via `recordUiEvent()` or the `POST /telemetry/event`
411
434
 
412
435
  The SDK automatically redacts sensitive keys from all captured payloads. Redaction is applied recursively up to 6 levels deep.
413
436
 
414
- **Default redacted keys:** `password`, `token`, `apiKey`, `secret`, `authorization`
437
+ **Default redacted keys:** `password`, `token`, `apiKey`, `secret`, `authorization`, `userId`, `userLocation`, `email`, `userEmail`, `phone`, `phoneNumber`
415
438
 
416
439
  **Custom redaction:**
417
440
 
@@ -457,6 +480,7 @@ Full Amplitude-style documentation is available in the [`docs/`](./docs/) direct
457
480
  | [Sessions](./docs/sessions.md) | `recordSession()`, always-capture behavior |
458
481
  | [Business Events](./docs/business-events.md) | `recordBusinessEvent()` |
459
482
  | [UI Events](./docs/ui-events.md) | `recordUiEvent()` + POST endpoint |
483
+ | [Browser Client](./docs/client.md) | `TelemetryClient` for browser apps |
460
484
  | [Sinks](./docs/sinks.md) | InMemorySink, RemoteSink, ConsoleSink, combineSinks |
461
485
  | [Endpoints](./docs/endpoints.md) | Dashboard, JSON API, event ingestion |
462
486
  | [Easy Setup](./docs/easy-setup.md) | `createMcpObservabilityEasy()` deep-dive |
@@ -522,7 +546,8 @@ mcp-observability/
522
546
  │ └── publish.yml # Publish to npm on release
523
547
  ├── docs/ # Comprehensive SDK documentation
524
548
  ├── src/
525
- │ ├── index.ts # Core SDK: types, classes, analytics
549
+ │ ├── index.ts # Core SDK: types, classes, metrics
550
+ │ ├── client.ts # Browser telemetry client
526
551
  │ ├── endpoints.ts # Express router and middleware
527
552
  │ ├── easy-setup.ts # Simplified configuration factory
528
553
  │ └── remote-sink.ts # Remote event batching and transmission
@@ -539,10 +564,11 @@ mcp-observability/
539
564
 
540
565
  | File | Description |
541
566
  | --- | --- |
542
- | `src/index.ts` | Core SDK -- event types, `McpObservability` class, `InMemorySink`, sampling, sanitization, analytics aggregation |
543
- | `src/endpoints.ts` | `expressTelemetry()` middleware and `createTelemetryRouter()` for JSON metrics, HTML dashboard, and event ingestion |
567
+ | `src/index.ts` | Core SDK -- event types, `McpObservability` class, `InMemorySink`, sampling, sanitization |
568
+ | `src/client.ts` | Browser telemetry client -- `TelemetryClient` class with batching, session management, `sendBeacon` fallback |
569
+ | `src/endpoints.ts` | `expressTelemetry()` middleware and `createTelemetryRouter(express)` for JSON metrics, HTML dashboard, and event ingestion |
544
570
  | `src/easy-setup.ts` | `createMcpObservabilityEasy()` -- auto-configures sinks and sampling based on environment |
545
- | `src/remote-sink.ts` | `RemoteSink` class -- batches events and sends to Listo Insights API with retry logic |
571
+ | `src/remote-sink.ts` | `RemoteSink` class -- batches events and sends to remote API with retry logic |
546
572
 
547
573
  ## CI/CD and Publishing
548
574
 
@@ -583,14 +609,13 @@ The tag, npm publish, and GitHub Release are all handled automatically by CI.
583
609
 
584
610
  - Ensure `enableLocalDashboard: true` is set (default in dev)
585
611
  - Visit `http://localhost:PORT/telemetry/dashboard`
586
- - Confirm `createTelemetryRouter()` is mounted: `app.use('/telemetry', createTelemetryRouter())`
612
+ - Confirm `createTelemetryRouter(express)` is mounted: `app.use('/telemetry', createTelemetryRouter(express))`
587
613
 
588
- ### Data not reaching Listo Insights
614
+ ### Data not reaching remote endpoint
589
615
 
590
- - Verify `INSIGHTS_API_KEY` is set and valid (starts with `fuse_`)
591
- - Check `INSIGHTS_API_URL` is correct
592
- - Look for `[Listo Insights]` warnings in server logs
593
- - Generate a new API key at https://app.listoai.co/settings
616
+ - Verify `LISTO_API_KEY` is set and valid
617
+ - Check `LISTO_API_URL` is correct
618
+ - Look for warnings in server logs
594
619
 
595
620
  ### Performance overhead
596
621
 
@@ -0,0 +1,41 @@
1
+ export interface TelemetryClientOptions {
2
+ endpoint: string;
3
+ sessionId?: string;
4
+ tenantId?: string;
5
+ locale?: string;
6
+ batchSize?: number;
7
+ flushIntervalMs?: number;
8
+ onError?: (error: unknown) => void;
9
+ debug?: boolean;
10
+ }
11
+ export declare class TelemetryClient {
12
+ private readonly endpoint;
13
+ private readonly sessionId;
14
+ private readonly batchSize;
15
+ private readonly flushIntervalMs;
16
+ private readonly onError;
17
+ private readonly debug;
18
+ private queue;
19
+ private timer;
20
+ private context;
21
+ private destroyed;
22
+ constructor(options: TelemetryClientOptions);
23
+ track(name: string, properties?: Record<string, unknown>): void;
24
+ trackUi(name: string, options: {
25
+ action?: string;
26
+ widgetId?: string;
27
+ toolName?: string;
28
+ properties?: Record<string, unknown>;
29
+ }): void;
30
+ setContext(ctx: {
31
+ tenantId?: string;
32
+ locale?: string;
33
+ }): void;
34
+ flush(): void;
35
+ destroy(): void;
36
+ private enqueue;
37
+ private send;
38
+ private handleVisibilityChange;
39
+ }
40
+ export declare function createTelemetryClient(options: TelemetryClientOptions): TelemetryClient;
41
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAcD,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2B;IACnD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAU;IAChC,OAAO,CAAC,KAAK,CAAwB;IACrC,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,OAAO,CAAyC;IACxD,OAAO,CAAC,SAAS,CAAS;gBAEd,OAAO,EAAE,sBAAsB;IAsB3C,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAY/D,OAAO,CACL,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;QACP,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACtC,GACA,IAAI;IAeP,UAAU,CAAC,GAAG,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAK7D,KAAK,IAAI,IAAI;IAMb,OAAO,IAAI,IAAI;IAef,OAAO,CAAC,OAAO;IAOf,OAAO,CAAC,IAAI;IA8BZ,OAAO,CAAC,sBAAsB,CAqB5B;CACH;AAED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,sBAAsB,GAC9B,eAAe,CAEjB"}
package/dist/client.js ADDED
@@ -0,0 +1,138 @@
1
+ export class TelemetryClient {
2
+ endpoint;
3
+ sessionId;
4
+ batchSize;
5
+ flushIntervalMs;
6
+ onError;
7
+ debug;
8
+ queue = [];
9
+ timer = null;
10
+ context;
11
+ destroyed = false;
12
+ constructor(options) {
13
+ this.endpoint = options.endpoint.replace(/\/+$/, '');
14
+ this.sessionId = options.sessionId ?? globalThis.crypto.randomUUID();
15
+ this.batchSize = options.batchSize ?? 20;
16
+ this.flushIntervalMs = options.flushIntervalMs ?? 5000;
17
+ this.onError = options.onError ?? (() => { });
18
+ this.debug = options.debug ?? false;
19
+ this.context = {
20
+ tenantId: options.tenantId,
21
+ locale: options.locale,
22
+ };
23
+ this.timer = setInterval(() => this.flush(), this.flushIntervalMs);
24
+ if (typeof globalThis.document !== 'undefined') {
25
+ globalThis.document.addEventListener('visibilitychange', this.handleVisibilityChange);
26
+ }
27
+ }
28
+ track(name, properties) {
29
+ if (this.destroyed)
30
+ return;
31
+ this.enqueue({
32
+ name,
33
+ timestamp: Date.now(),
34
+ sessionId: this.sessionId,
35
+ tenantId: this.context.tenantId,
36
+ locale: this.context.locale,
37
+ properties,
38
+ });
39
+ }
40
+ trackUi(name, options) {
41
+ if (this.destroyed)
42
+ return;
43
+ this.enqueue({
44
+ name,
45
+ timestamp: Date.now(),
46
+ sessionId: this.sessionId,
47
+ tenantId: this.context.tenantId,
48
+ locale: this.context.locale,
49
+ action: options.action,
50
+ widgetId: options.widgetId,
51
+ toolName: options.toolName,
52
+ properties: options.properties,
53
+ });
54
+ }
55
+ setContext(ctx) {
56
+ if (ctx.tenantId !== undefined)
57
+ this.context.tenantId = ctx.tenantId;
58
+ if (ctx.locale !== undefined)
59
+ this.context.locale = ctx.locale;
60
+ }
61
+ flush() {
62
+ if (this.queue.length === 0)
63
+ return;
64
+ const events = this.queue.splice(0);
65
+ this.send(events);
66
+ }
67
+ destroy() {
68
+ this.destroyed = true;
69
+ if (this.timer !== null) {
70
+ clearInterval(this.timer);
71
+ this.timer = null;
72
+ }
73
+ if (typeof globalThis.document !== 'undefined') {
74
+ globalThis.document.removeEventListener('visibilitychange', this.handleVisibilityChange);
75
+ }
76
+ this.flush();
77
+ }
78
+ enqueue(event) {
79
+ this.queue.push(event);
80
+ if (this.queue.length >= this.batchSize) {
81
+ this.flush();
82
+ }
83
+ }
84
+ send(events) {
85
+ const url = `${this.endpoint}/events`;
86
+ const body = JSON.stringify({ events });
87
+ if (this.debug) {
88
+ console.debug('[telemetry-client] sending', events.length, 'events');
89
+ }
90
+ try {
91
+ fetch(url, {
92
+ method: 'POST',
93
+ headers: { 'Content-Type': 'application/json' },
94
+ body,
95
+ keepalive: true,
96
+ }).catch((error) => {
97
+ try {
98
+ this.onError(error);
99
+ }
100
+ catch {
101
+ // never throw from onError
102
+ }
103
+ });
104
+ }
105
+ catch (error) {
106
+ try {
107
+ this.onError(error);
108
+ }
109
+ catch {
110
+ // never throw from onError
111
+ }
112
+ }
113
+ }
114
+ handleVisibilityChange = () => {
115
+ if (globalThis.document?.visibilityState === 'hidden') {
116
+ const events = this.queue.splice(0);
117
+ if (events.length === 0)
118
+ return;
119
+ const url = `${this.endpoint}/events`;
120
+ const body = JSON.stringify({ events });
121
+ if (typeof navigator?.sendBeacon === 'function') {
122
+ try {
123
+ navigator.sendBeacon(url, new Blob([body], { type: 'application/json' }));
124
+ }
125
+ catch {
126
+ this.send(events);
127
+ }
128
+ }
129
+ else {
130
+ this.send(events);
131
+ }
132
+ }
133
+ };
134
+ }
135
+ export function createTelemetryClient(options) {
136
+ return new TelemetryClient(options);
137
+ }
138
+ //# sourceMappingURL=client.js.map
@@ -2,17 +2,21 @@ export interface EasySetupConfig {
2
2
  serviceName: string;
3
3
  serviceVersion?: string;
4
4
  environment?: 'dev' | 'staging' | 'production';
5
+ /** @deprecated Use `listoApiUrl` instead. */
5
6
  insightsApiUrl?: string;
7
+ /** @deprecated Use `listoApiKey` instead. */
6
8
  insightsApiKey?: string;
9
+ listoApiUrl?: string;
10
+ listoApiKey?: string;
7
11
  enableLocalDashboard?: boolean;
8
12
  sampleRate?: number;
9
13
  }
10
14
  /**
11
15
  * Create an observability instance with environment-based defaults.
12
16
  *
13
- * This is the easiest way to get started with Listo Insights.
17
+ * This is the easiest way to get started.
14
18
  * It automatically:
15
- * - Configures RemoteSink if INSIGHTS_API_KEY is provided
19
+ * - Configures RemoteSink if LISTO_API_KEY is provided
16
20
  * - Configures InMemorySink + local dashboard in development
17
21
  * - Applies sensible defaults (sampling, redaction, etc.)
18
22
  * - Auto-detects tenant/user from common headers
@@ -26,8 +30,8 @@ export interface EasySetupConfig {
26
30
  * ```
27
31
  *
28
32
  * Environment variables used:
29
- * - INSIGHTS_API_URL: Insights API endpoint (default: https://api.listoai.co)
30
- * - INSIGHTS_API_KEY: API key for authentication (generated in dashboard)
33
+ * - LISTO_API_URL: Remote telemetry API endpoint
34
+ * - LISTO_API_KEY: API key for authentication
31
35
  * - NODE_ENV: Determines if dev/production mode
32
36
  */
33
37
  export declare function createMcpObservabilityEasy(config: EasySetupConfig): import("./index.js").McpObservability;
@@ -1 +1 @@
1
- {"version":3,"file":"easy-setup.d.ts","sourceRoot":"","sources":["../src/easy-setup.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,YAAY,CAAC;IAC/C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,eAAe,yCAsEjE"}
1
+ {"version":3,"file":"easy-setup.d.ts","sourceRoot":"","sources":["../src/easy-setup.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,YAAY,CAAC;IAC/C,6CAA6C;IAC7C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,6CAA6C;IAC7C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,eAAe,yCAyEjE"}
@@ -4,9 +4,9 @@ import { TELEMETRY_SINK_KEY, OBSERVABILITY_KEY, setGlobal, } from './endpoints.j
4
4
  /**
5
5
  * Create an observability instance with environment-based defaults.
6
6
  *
7
- * This is the easiest way to get started with Listo Insights.
7
+ * This is the easiest way to get started.
8
8
  * It automatically:
9
- * - Configures RemoteSink if INSIGHTS_API_KEY is provided
9
+ * - Configures RemoteSink if LISTO_API_KEY is provided
10
10
  * - Configures InMemorySink + local dashboard in development
11
11
  * - Applies sensible defaults (sampling, redaction, etc.)
12
12
  * - Auto-detects tenant/user from common headers
@@ -20,12 +20,16 @@ import { TELEMETRY_SINK_KEY, OBSERVABILITY_KEY, setGlobal, } from './endpoints.j
20
20
  * ```
21
21
  *
22
22
  * Environment variables used:
23
- * - INSIGHTS_API_URL: Insights API endpoint (default: https://api.listoai.co)
24
- * - INSIGHTS_API_KEY: API key for authentication (generated in dashboard)
23
+ * - LISTO_API_URL: Remote telemetry API endpoint
24
+ * - LISTO_API_KEY: API key for authentication
25
25
  * - NODE_ENV: Determines if dev/production mode
26
26
  */
27
27
  export function createMcpObservabilityEasy(config) {
28
- const { serviceName, serviceVersion = '1.0.0', environment = process.env.NODE_ENV === 'production' ? 'production' : 'dev', insightsApiUrl = process.env.INSIGHTS_API_URL, insightsApiKey = process.env.INSIGHTS_API_KEY, enableLocalDashboard = environment === 'dev', sampleRate = environment === 'production' ? 0.1 : 1.0, } = config;
28
+ const { serviceName, serviceVersion = '1.0.0', environment = process.env.NODE_ENV === 'production' ? 'production' : 'dev', listoApiUrl = config.insightsApiUrl ??
29
+ process.env.LISTO_API_URL ??
30
+ process.env.INSIGHTS_API_URL, listoApiKey = config.insightsApiKey ??
31
+ process.env.LISTO_API_KEY ??
32
+ process.env.INSIGHTS_API_KEY, enableLocalDashboard = environment === 'dev', sampleRate = environment === 'production' ? 0.1 : 1.0, } = config;
29
33
  const sinks = [];
30
34
  // Console logging in dev only
31
35
  if (environment === 'dev') {
@@ -38,20 +42,19 @@ export function createMcpObservabilityEasy(config) {
38
42
  // Export globally for telemetry endpoints to access
39
43
  setGlobal(TELEMETRY_SINK_KEY, telemetrySink);
40
44
  }
41
- // Remote Listo Insights sink
42
- if (insightsApiUrl && insightsApiKey) {
45
+ // Remote telemetry sink
46
+ if (listoApiUrl && listoApiKey) {
43
47
  sinks.push(createRemoteSink({
44
- endpoint: `${insightsApiUrl}/v1/events/batch`,
45
- apiKey: insightsApiKey,
48
+ endpoint: `${listoApiUrl}/v1/events/batch`,
49
+ apiKey: listoApiKey,
46
50
  batchSize: 50,
47
51
  flushIntervalMs: 5000,
48
52
  maxRetries: 3,
49
53
  }));
50
54
  }
51
55
  else if (environment !== 'dev') {
52
- console.warn('[Listo Insights] No API key configured. ' +
53
- 'Set INSIGHTS_API_KEY environment variable to enable remote telemetry. ' +
54
- 'Generate a key at: https://app.listoai.co/settings');
56
+ console.warn('[telemetry] No API key configured. ' +
57
+ 'Set LISTO_API_KEY environment variable to enable remote telemetry.');
55
58
  }
56
59
  // Create the observability instance
57
60
  const options = {
@@ -60,7 +63,7 @@ export function createMcpObservabilityEasy(config) {
60
63
  environment,
61
64
  sampleRate,
62
65
  capturePayloads: true,
63
- redactKeys: ['password', 'token', 'apiKey', 'secret', 'authorization'],
66
+ // Omit redactKeys to use the SDK defaults (includes PII keys)
64
67
  sinks: sinks.length > 0 ? sinks : [createConsoleSink({ logSuccess: false })],
65
68
  // Auto-detect tenant from common headers
66
69
  tenantResolver: (req) => req.headers['x-tenant-id'] ||
@@ -22,13 +22,17 @@ export declare function expressTelemetry(observability: McpObservability): (req:
22
22
  * - GET /telemetry/dashboard - HTML dashboard
23
23
  * - POST /telemetry/event - UI event ingestion
24
24
  *
25
+ * @param expressModule - The express module instance. Required because the SDK
26
+ * cannot resolve express from the consumer's node_modules automatically.
27
+ *
25
28
  * @example
26
29
  * ```typescript
30
+ * import express from 'express';
27
31
  * import { createTelemetryRouter } from '@listo-ai/mcp-observability';
28
32
  *
29
- * app.use('/telemetry', createTelemetryRouter());
33
+ * app.use('/telemetry', createTelemetryRouter(express));
30
34
  * ```
31
35
  */
32
- export declare function createTelemetryRouter(): any;
36
+ export declare function createTelemetryRouter(expressModule: any): any;
33
37
  export { TELEMETRY_SINK_KEY, OBSERVABILITY_KEY, setGlobal };
34
38
  //# sourceMappingURL=endpoints.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"endpoints.d.ts","sourceRoot":"","sources":["../src/endpoints.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,KAAK,EAAE,gBAAgB,EAAgB,MAAM,YAAY,CAAC;AAEjE,QAAA,MAAM,kBAAkB,eAAoC,CAAC;AAC7D,QAAA,MAAM,iBAAiB,eAAoC,CAAC;AAe5D,iBAAS,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAEjD;AA4BD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,gBAAgB,IAChD,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,mBAW9D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,QAqQpC;AAED,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,SAAS,EAAE,CAAC"}
1
+ {"version":3,"file":"endpoints.d.ts","sourceRoot":"","sources":["../src/endpoints.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,KAAK,EAAE,gBAAgB,EAAgB,MAAM,YAAY,CAAC;AAEjE,QAAA,MAAM,kBAAkB,eAAsC,CAAC;AAC/D,QAAA,MAAM,iBAAiB,eAAsC,CAAC;AAe9D,iBAAS,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAEjD;AA4BD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,gBAAgB,IAChD,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,mBAW9D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AAEH,wBAAgB,qBAAqB,CAAC,aAAa,EAAE,GAAG,OAySvD;AAED,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,SAAS,EAAE,CAAC"}
package/dist/endpoints.js CHANGED
@@ -1,5 +1,5 @@
1
- const TELEMETRY_SINK_KEY = Symbol.for('listo.telemetrySink');
2
- const OBSERVABILITY_KEY = Symbol.for('listo.observability');
1
+ const TELEMETRY_SINK_KEY = Symbol.for('mcp-obs.telemetrySink');
2
+ const OBSERVABILITY_KEY = Symbol.for('mcp-obs.observability');
3
3
  function escapeHtml(str) {
4
4
  return str
5
5
  .replace(/&/g, '&amp;')
@@ -71,17 +71,20 @@ export function expressTelemetry(observability) {
71
71
  * - GET /telemetry/dashboard - HTML dashboard
72
72
  * - POST /telemetry/event - UI event ingestion
73
73
  *
74
+ * @param expressModule - The express module instance. Required because the SDK
75
+ * cannot resolve express from the consumer's node_modules automatically.
76
+ *
74
77
  * @example
75
78
  * ```typescript
79
+ * import express from 'express';
76
80
  * import { createTelemetryRouter } from '@listo-ai/mcp-observability';
77
81
  *
78
- * app.use('/telemetry', createTelemetryRouter());
82
+ * app.use('/telemetry', createTelemetryRouter(express));
79
83
  * ```
80
84
  */
81
- export function createTelemetryRouter() {
82
- // eslint-disable-next-line @typescript-eslint/no-require-imports
83
- const express = require('express');
84
- const router = express.Router();
85
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
86
+ export function createTelemetryRouter(expressModule) {
87
+ const router = expressModule.Router();
85
88
  const getTelemetrySink = () => {
86
89
  return getGlobal(TELEMETRY_SINK_KEY);
87
90
  };
@@ -110,14 +113,14 @@ export function createTelemetryRouter() {
110
113
  <!DOCTYPE html>
111
114
  <html>
112
115
  <head>
113
- <title>Listo Telemetry</title>
116
+ <title>Telemetry Dashboard</title>
114
117
  <style>
115
118
  body { font-family: system-ui, sans-serif; padding: 20px; }
116
119
  .error { color: #d32f2f; }
117
120
  </style>
118
121
  </head>
119
122
  <body>
120
- <h1>Listo Telemetry Dashboard</h1>
123
+ <h1>Telemetry Dashboard</h1>
121
124
  <p class="error">Local telemetry is not enabled.</p>
122
125
  <p>To enable, set enableLocalDashboard: true in createMcpObservabilityEasy config.</p>
123
126
  </body>
@@ -131,7 +134,7 @@ export function createTelemetryRouter() {
131
134
  <head>
132
135
  <meta charset="UTF-8">
133
136
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
134
- <title>Listo Telemetry Dashboard</title>
137
+ <title>Telemetry Dashboard</title>
135
138
  <style>
136
139
  * { margin: 0; padding: 0; box-sizing: border-box; }
137
140
  body {
@@ -186,7 +189,7 @@ export function createTelemetryRouter() {
186
189
  <small style="margin-left: 10px; color: #666;">Auto-refreshes every 5 seconds</small>
187
190
  </div>
188
191
 
189
- <h1>📊 Listo Telemetry Dashboard</h1>
192
+ <h1>Telemetry Dashboard</h1>
190
193
 
191
194
  <div class="grid">
192
195
  <div class="card">
@@ -270,8 +273,7 @@ export function createTelemetryRouter() {
270
273
  : ''}
271
274
 
272
275
  <p style="margin-top: 40px; color: #999; font-size: 12px;">
273
- Last updated: ${new Date().toLocaleTimeString()} |
274
- View in <a href="https://app.listoai.co" target="_blank" style="color: #1976d2;">Listo</a>
276
+ Last updated: ${new Date().toLocaleTimeString()}
275
277
  </p>
276
278
  </div>
277
279
 
@@ -287,7 +289,7 @@ export function createTelemetryRouter() {
287
289
  .send(html);
288
290
  });
289
291
  // UI event ingestion endpoint
290
- router.post('/event', express.json({ limit: MAX_EVENT_BODY_SIZE }), (req, res) => {
292
+ router.post('/event', expressModule.json({ limit: MAX_EVENT_BODY_SIZE }), (req, res) => {
291
293
  const validated = validateUiEventBody(req.body);
292
294
  if (!validated) {
293
295
  res
@@ -301,6 +303,35 @@ export function createTelemetryRouter() {
301
303
  }
302
304
  res.status(201).json({ ok: true });
303
305
  });
306
+ // Batch UI event ingestion endpoint
307
+ router.post('/events', expressModule.json({ limit: MAX_EVENT_BODY_SIZE * 100 }), (req, res) => {
308
+ const body = req.body;
309
+ if (!body || !Array.isArray(body.events)) {
310
+ res.status(400).json({ error: 'Expected { events: [...] } array' });
311
+ return;
312
+ }
313
+ const events = body.events;
314
+ if (events.length > 100) {
315
+ res.status(400).json({ error: 'Maximum 100 events per batch' });
316
+ return;
317
+ }
318
+ const observability = getGlobal(OBSERVABILITY_KEY);
319
+ let accepted = 0;
320
+ let rejected = 0;
321
+ for (const raw of events) {
322
+ const validated = validateUiEventBody(raw);
323
+ if (validated) {
324
+ if (observability) {
325
+ observability.recordUiEvent(validated);
326
+ }
327
+ accepted++;
328
+ }
329
+ else {
330
+ rejected++;
331
+ }
332
+ }
333
+ res.status(201).json({ ok: true, accepted, rejected });
334
+ });
304
335
  return router;
305
336
  }
306
337
  export { TELEMETRY_SINK_KEY, OBSERVABILITY_KEY, setGlobal };
package/dist/index.d.ts CHANGED
@@ -209,66 +209,6 @@ export declare class InMemorySink implements EventSink {
209
209
  error: number;
210
210
  }>;
211
211
  };
212
- analytics: {
213
- search: {
214
- topQueries: CountEntry[];
215
- topKeywords: CountEntry[];
216
- minRatings: CountEntry[];
217
- priceLevels: CountEntry[];
218
- neighborhoods: CountEntry[];
219
- styles: CountEntry[];
220
- limits: CountEntry[];
221
- };
222
- geo: {
223
- topCities: CountEntry[];
224
- topCountries: CountEntry[];
225
- };
226
- hotels: {
227
- topViewed: {
228
- id: string;
229
- name?: string;
230
- count: number;
231
- }[];
232
- topBookingClicks: {
233
- id: string;
234
- name?: string;
235
- count: number;
236
- }[];
237
- topBookingCtr: {
238
- id: string;
239
- name: string | undefined;
240
- clicks: number;
241
- views: number;
242
- ctr: number | null;
243
- }[];
244
- topWebsiteClicks: {
245
- id: string;
246
- name?: string;
247
- count: number;
248
- }[];
249
- viewedPriceLevels: CountEntry[];
250
- viewedStyles: CountEntry[];
251
- viewedCategories: CountEntry[];
252
- viewedNeighborhoods: CountEntry[];
253
- };
254
- ctr: {
255
- impressions: number;
256
- clicks: number;
257
- detailViews: number;
258
- bookingClicks: number;
259
- websiteClicks: number;
260
- bookingPerImpression: number | null;
261
- bookingPerDetailView: number | null;
262
- };
263
- sessions: {
264
- total: number;
265
- converted: number;
266
- booking: number;
267
- website: number;
268
- conversionRate: number | null;
269
- unknownSessionUiEvents: number;
270
- };
271
- };
272
212
  };
273
213
  }
274
214
  export declare function createConsoleSink(options?: {
@@ -295,10 +235,6 @@ export declare class McpObservability {
295
235
  private safeEmit;
296
236
  }
297
237
  export declare function createMcpObservability(options: ObservabilityOptions): McpObservability;
298
- type CountEntry = {
299
- value: string;
300
- count: number;
301
- };
302
238
  export { RemoteSink, createRemoteSink, type RemoteSinkOptions, } from './remote-sink.js';
303
239
  export { createMcpObservabilityEasy, type EasySetupConfig, } from './easy-setup.js';
304
240
  export { createTelemetryRouter, expressTelemetry } from './endpoints.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,MAAM,MAAM,WAAW,GAAG,IAAI,GAAG,OAAO,CAAC;AAEzC,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,cAAc,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,WAAW,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,aAAa,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,WAAW,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,aAAa,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,gBAAgB,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG;IACpB,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAC1B,gBAAgB,GAChB,eAAe,GACf,eAAe,GACf,aAAa,GACb,OAAO,CAAC;AAEZ,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3D,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,MAAM,GAAG,SAAS,CAAC;CAC/D,CAAC;AAEF,KAAK,mBAAmB,GAAG;IACzB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,SAAS,EAAE,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;CAC/C,CAAC;AAEF,KAAK,kBAAkB,GAAG;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;CAC9C,CAAC;AAEF,qBAAa,YAAa,YAAW,SAAS;IAC5C,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;gBAEnB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE;IAIxC,IAAI,CAAC,KAAK,EAAE,kBAAkB;IAO9B,SAAS,IAAI,kBAAkB,EAAE;IAIjC,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBA6d4B,MAAM;oBAAM,MAAM;uBAAS,MAAM;;;;;;;;;;;;;;;;;;;wBAyF1D,MAAM;2BAAS,MAAM;2BAAS,MAAM;;;wBAApC,MAAM;2BAAS,MAAM;2BAAS,MAAM;;;;;;;;;;wBAApC,MAAM;2BAAS,MAAM;2BAAS,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;CA3gBlD;AAED,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE;IAC1C,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,GAAG,SAAS,CAiCZ;AAED,wBAAgB,YAAY,CAAC,GAAG,KAAK,EAAE,SAAS,EAAE,GAAG,SAAS,CAiB7D;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAc;IACzC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAU;IAC1C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAER;gBACZ,OAAO,EAAE,oBAAoB;IAuBzC,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,SAAS,GAAG,MAAM,CAAC;IAY9D,gBAAgB,CACpB,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,OAAO,EAAE,CAAC,GAAG,EAAE,mBAAmB,KAAK,OAAO,CAAC,IAAI,CAAC;IAsDtD,cAAc,CAAC,QAAQ,EAAE,SAAS,EAChC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,CAAC,SAAS,CAAC,GAAG,SAAS,EAC9D,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,kBAAkB,GAClD,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,CAAC,SAAS,CAAC;IA6E5C,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,eAAe,EAAE,WAAW,GAAG,SAAS,CAAC;IAWnE,mBAAmB,CACjB,IAAI,EAAE,MAAM,EACZ,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,MAAM,CAAC,EAAE,WAAW;IAetB,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,eAAe;IAsBvB,OAAO,CAAC,QAAQ;CAejB;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,oBAAoB,oBAEnE;AAwJD,KAAK,UAAU,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AA+WnD,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,KAAK,iBAAiB,GACvB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,0BAA0B,EAC1B,KAAK,eAAe,GACrB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -40,7 +40,6 @@ export class InMemorySink {
40
40
  sessions: summarizeSessionEvents(sessionEvents),
41
41
  ui: summarizeUiEvents(uiEvents),
42
42
  business: summarizeBusinessEvents(businessEvents),
43
- analytics: computeAnalytics({ uiEvents, businessEvents }),
44
43
  };
45
44
  }
46
45
  }
@@ -112,6 +111,12 @@ export class McpObservability {
112
111
  'password',
113
112
  'token',
114
113
  'secret',
114
+ 'userid',
115
+ 'userlocation',
116
+ 'email',
117
+ 'useremail',
118
+ 'phone',
119
+ 'phonenumber',
115
120
  ]).map((key) => key.toLowerCase()));
116
121
  this.capturePayloads = options.capturePayloads ?? false;
117
122
  this.tenantResolver = options.tenantResolver;
@@ -124,6 +129,9 @@ export class McpObservability {
124
129
  service: this.serviceName,
125
130
  serviceVersion: this.serviceVersion,
126
131
  environment: this.environment,
132
+ properties: event.properties
133
+ ? this.sanitizePayload(event.properties)
134
+ : undefined,
127
135
  };
128
136
  this.safeEmit(enriched);
129
137
  }
@@ -273,7 +281,9 @@ export class McpObservability {
273
281
  serviceVersion: this.serviceVersion,
274
282
  environment: this.environment,
275
283
  name,
276
- properties,
284
+ properties: properties
285
+ ? this.sanitizePayload(properties)
286
+ : undefined,
277
287
  status,
278
288
  };
279
289
  this.safeEmit(event);
@@ -469,321 +479,6 @@ function truncatePreview(value) {
469
479
  return truncateString(value);
470
480
  return value;
471
481
  }
472
- function incMap(map, rawKey, delta = 1) {
473
- const key = rawKey.trim();
474
- if (!key)
475
- return;
476
- map.set(key, (map.get(key) ?? 0) + delta);
477
- }
478
- function asString(value) {
479
- if (typeof value === 'string')
480
- return value;
481
- if (typeof value === 'number' || typeof value === 'boolean')
482
- return String(value);
483
- return undefined;
484
- }
485
- function asObject(value) {
486
- if (!value || typeof value !== 'object' || Array.isArray(value))
487
- return undefined;
488
- return value;
489
- }
490
- function asStringArray(value) {
491
- if (!Array.isArray(value))
492
- return [];
493
- return value
494
- .map((v) => asString(v))
495
- .filter((v) => typeof v === 'string' && v.trim().length > 0);
496
- }
497
- function tokenize(input, stopWords) {
498
- const sanitized = input
499
- .toLowerCase()
500
- .replace(/[^a-z0-9]+/g, ' ')
501
- .trim();
502
- if (!sanitized)
503
- return [];
504
- return sanitized
505
- .split(/\s+/)
506
- .map((token) => token.trim())
507
- .filter((token) => token.length >= 3)
508
- .filter((token) => !stopWords.has(token));
509
- }
510
- function topEntries(map, limit = 10) {
511
- return Array.from(map.entries())
512
- .sort((a, b) => b[1] - a[1])
513
- .slice(0, limit)
514
- .map(([value, count]) => ({ value, count }));
515
- }
516
- function topHotelEntries(map, limit = 10) {
517
- return Array.from(map.entries())
518
- .sort((a, b) => b[1].count - a[1].count)
519
- .slice(0, limit)
520
- .map(([id, meta]) => ({ id, name: meta.name, count: meta.count }));
521
- }
522
- function computeAnalytics(options) {
523
- const queryCounts = new Map();
524
- const keywordCounts = new Map();
525
- const minRatingCounts = new Map();
526
- const cityCounts = new Map();
527
- const countryCounts = new Map();
528
- const hotelViewCounts = new Map();
529
- const bookingClickCounts = new Map();
530
- const websiteClickCounts = new Map();
531
- const searchPriceCounts = new Map();
532
- const searchStyleCounts = new Map();
533
- const searchNeighborhoodCounts = new Map();
534
- const searchLimitCounts = new Map();
535
- const viewedPriceCounts = new Map();
536
- const viewedStyleCounts = new Map();
537
- const viewedCategoryCounts = new Map();
538
- const viewedNeighborhoodCounts = new Map();
539
- const stopWords = new Set([
540
- 'a',
541
- 'an',
542
- 'and',
543
- 'are',
544
- 'as',
545
- 'at',
546
- 'be',
547
- 'but',
548
- 'by',
549
- 'for',
550
- 'from',
551
- 'has',
552
- 'have',
553
- 'hotel',
554
- 'hotels',
555
- 'i',
556
- 'in',
557
- 'is',
558
- 'it',
559
- 'of',
560
- 'on',
561
- 'or',
562
- 'the',
563
- 'to',
564
- 'with',
565
- ]);
566
- const recordQuery = (query) => {
567
- const normalized = query.trim();
568
- if (!normalized)
569
- return;
570
- incMap(queryCounts, normalized);
571
- tokenize(normalized, stopWords).forEach((token) => incMap(keywordCounts, token));
572
- };
573
- const recordMinRating = (value) => {
574
- const str = asString(value);
575
- if (!str)
576
- return;
577
- incMap(minRatingCounts, str);
578
- };
579
- const recordCityCountry = (city, country) => {
580
- if (city)
581
- incMap(cityCounts, city);
582
- if (country)
583
- incMap(countryCounts, country);
584
- };
585
- const recordHotelView = (hotelId, name) => {
586
- if (!hotelId)
587
- return;
588
- const existing = hotelViewCounts.get(hotelId) ?? {
589
- count: 0,
590
- name: undefined,
591
- };
592
- hotelViewCounts.set(hotelId, {
593
- count: existing.count + 1,
594
- name: existing.name ?? (name || undefined),
595
- });
596
- };
597
- const recordHotelClick = (map, hotelId, name) => {
598
- if (!hotelId)
599
- return;
600
- const existing = map.get(hotelId) ?? { count: 0, name: undefined };
601
- map.set(hotelId, {
602
- count: existing.count + 1,
603
- name: existing.name ?? (name || undefined),
604
- });
605
- };
606
- const recordList = (map, values) => {
607
- const list = asStringArray(values);
608
- list.forEach((value) => incMap(map, value));
609
- };
610
- // Business events (tool-level signals)
611
- for (const event of options.businessEvents) {
612
- if (event.name === 'hotel_search') {
613
- const q = asString(event.properties?.q);
614
- if (q)
615
- recordQuery(q);
616
- recordMinRating(event.properties?.minRating ?? event.properties?.min_rating);
617
- recordCityCountry(asString(event.properties?.city), asString(event.properties?.country));
618
- recordList(searchPriceCounts, event.properties?.priceLevels);
619
- recordList(searchStyleCounts, event.properties?.styles);
620
- recordList(searchNeighborhoodCounts, event.properties?.neighborhoods);
621
- const limit = asString(event.properties?.limit);
622
- if (limit)
623
- incMap(searchLimitCounts, limit);
624
- }
625
- if (event.name === 'hotel_details') {
626
- const hotelId = asString(event.properties?.id);
627
- const name = asString(event.properties?.name);
628
- recordHotelView(hotelId, name);
629
- recordCityCountry(asString(event.properties?.city), asString(event.properties?.country));
630
- const priceLevel = asString(event.properties?.priceLevel);
631
- if (priceLevel)
632
- incMap(viewedPriceCounts, priceLevel);
633
- recordList(viewedStyleCounts, event.properties?.styles);
634
- recordList(viewedCategoryCounts, event.properties?.categories);
635
- recordList(viewedNeighborhoodCounts, event.properties?.neighborhoods);
636
- }
637
- }
638
- // UI events (engagement/CTR)
639
- for (const event of options.uiEvents) {
640
- if (event.name === 'search_results_rendered') {
641
- const filters = asObject(event.properties?.rawFilters) ??
642
- asObject(event.properties?.filters);
643
- const q = asString(filters?.q ?? filters?.query);
644
- if (q)
645
- recordQuery(q);
646
- recordMinRating(filters?.minRating ?? filters?.min_rating);
647
- recordList(searchPriceCounts, filters?.priceLevels ?? filters?.price_levels);
648
- recordList(searchStyleCounts, filters?.styles);
649
- recordList(searchNeighborhoodCounts, filters?.neighborhoods);
650
- const limit = asString(filters?.limit);
651
- if (limit)
652
- incMap(searchLimitCounts, limit);
653
- }
654
- if (event.name === 'hotel_detail_viewed') {
655
- const props = asObject(event.properties);
656
- recordHotelView(asString(props?.hotelId), asString(props?.hotelName));
657
- recordCityCountry(asString(props?.city), asString(props?.country));
658
- const priceLevel = asString(props?.priceLevel);
659
- if (priceLevel)
660
- incMap(viewedPriceCounts, priceLevel);
661
- recordList(viewedStyleCounts, props?.styles);
662
- recordList(viewedCategoryCounts, props?.categories);
663
- recordList(viewedNeighborhoodCounts, props?.neighborhoods);
664
- }
665
- if (event.name === 'hotel_selected') {
666
- const props = asObject(event.properties);
667
- recordHotelView(asString(props?.hotelId), asString(props?.hotelName));
668
- }
669
- if (event.name === 'booking_cta_click') {
670
- const props = asObject(event.properties);
671
- const hotelId = asString(props?.hotelId);
672
- recordHotelClick(bookingClickCounts, hotelId, asString(props?.hotelName));
673
- }
674
- if (event.name === 'website_click') {
675
- const props = asObject(event.properties);
676
- const hotelId = asString(props?.hotelId);
677
- recordHotelClick(websiteClickCounts, hotelId, asString(props?.hotelName));
678
- }
679
- }
680
- const impressions = options.uiEvents.filter((e) => e.name === 'widget_impression').length;
681
- const clicks = options.uiEvents.filter((e) => e.name === 'widget_click').length;
682
- const detailViews = options.uiEvents.filter((e) => e.name === 'hotel_detail_viewed').length;
683
- const bookingClicks = Array.from(bookingClickCounts.values()).reduce((acc, entry) => acc + (entry.count || 0), 0);
684
- const websiteClicks = Array.from(websiteClickCounts.values()).reduce((acc, entry) => acc + (entry.count || 0), 0);
685
- // Backfill missing names from any known hotel views (older clients may omit hotelName).
686
- for (const [hotelId, meta] of bookingClickCounts.entries()) {
687
- if (!meta.name) {
688
- const known = hotelViewCounts.get(hotelId)?.name;
689
- if (known)
690
- bookingClickCounts.set(hotelId, { ...meta, name: known });
691
- }
692
- }
693
- for (const [hotelId, meta] of websiteClickCounts.entries()) {
694
- if (!meta.name) {
695
- const known = hotelViewCounts.get(hotelId)?.name;
696
- if (known)
697
- websiteClickCounts.set(hotelId, { ...meta, name: known });
698
- }
699
- }
700
- const ctrFromImpressions = impressions ? bookingClicks / impressions : null;
701
- const ctrFromDetails = detailViews ? bookingClicks / detailViews : null;
702
- const topBookingCtr = Array.from(bookingClickCounts.entries())
703
- .map(([hotelId, meta]) => {
704
- const clicks = meta.count || 0;
705
- const views = hotelViewCounts.get(hotelId)?.count ?? 0;
706
- const name = meta.name ?? hotelViewCounts.get(hotelId)?.name;
707
- return {
708
- id: hotelId,
709
- name,
710
- clicks,
711
- views,
712
- ctr: views ? clicks / views : null,
713
- };
714
- })
715
- .filter((row) => row.ctr !== null && row.views >= 2)
716
- .sort((a, b) => (b.ctr ?? 0) - (a.ctr ?? 0))
717
- .slice(0, 8);
718
- const sessionsAll = new Set();
719
- const sessionsFromImpressions = new Set();
720
- const sessionsBooking = new Set();
721
- const sessionsWebsite = new Set();
722
- let unknownSessionUiEvents = 0;
723
- for (const event of options.uiEvents) {
724
- const sid = event.sessionId;
725
- if (!sid) {
726
- unknownSessionUiEvents += 1;
727
- continue;
728
- }
729
- sessionsAll.add(sid);
730
- if (event.name === 'widget_impression')
731
- sessionsFromImpressions.add(sid);
732
- if (event.name === 'booking_cta_click')
733
- sessionsBooking.add(sid);
734
- if (event.name === 'website_click')
735
- sessionsWebsite.add(sid);
736
- }
737
- const totalSessions = sessionsFromImpressions.size > 0
738
- ? sessionsFromImpressions.size
739
- : sessionsAll.size;
740
- const convertedSessions = new Set([
741
- ...Array.from(sessionsBooking),
742
- ...Array.from(sessionsWebsite),
743
- ]).size;
744
- return {
745
- search: {
746
- topQueries: topEntries(queryCounts, 12),
747
- topKeywords: topEntries(keywordCounts, 18),
748
- minRatings: topEntries(minRatingCounts, 8),
749
- priceLevels: topEntries(searchPriceCounts, 10),
750
- neighborhoods: topEntries(searchNeighborhoodCounts, 10),
751
- styles: topEntries(searchStyleCounts, 10),
752
- limits: topEntries(searchLimitCounts, 8),
753
- },
754
- geo: {
755
- topCities: topEntries(cityCounts, 12),
756
- topCountries: topEntries(countryCounts, 12),
757
- },
758
- hotels: {
759
- topViewed: topHotelEntries(hotelViewCounts, 12),
760
- topBookingClicks: topHotelEntries(bookingClickCounts, 12),
761
- topBookingCtr,
762
- topWebsiteClicks: topHotelEntries(websiteClickCounts, 12),
763
- viewedPriceLevels: topEntries(viewedPriceCounts, 10),
764
- viewedStyles: topEntries(viewedStyleCounts, 10),
765
- viewedCategories: topEntries(viewedCategoryCounts, 10),
766
- viewedNeighborhoods: topEntries(viewedNeighborhoodCounts, 10),
767
- },
768
- ctr: {
769
- impressions,
770
- clicks,
771
- detailViews,
772
- bookingClicks,
773
- websiteClicks,
774
- bookingPerImpression: ctrFromImpressions,
775
- bookingPerDetailView: ctrFromDetails,
776
- },
777
- sessions: {
778
- total: totalSessions,
779
- converted: convertedSessions,
780
- booking: sessionsBooking.size,
781
- website: sessionsWebsite.size,
782
- conversionRate: totalSessions ? convertedSessions / totalSessions : null,
783
- unknownSessionUiEvents,
784
- },
785
- };
786
- }
787
482
  export { RemoteSink, createRemoteSink, } from './remote-sink.js';
788
483
  export { createMcpObservabilityEasy, } from './easy-setup.js';
789
484
  export { createTelemetryRouter, expressTelemetry } from './endpoints.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@listo-ai/mcp-observability",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Lightweight telemetry SDK for MCP servers and web applications. Captures HTTP requests, MCP tool invocations, business events, and UI interactions with built-in payload sanitization.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,6 +9,10 @@
9
9
  ".": {
10
10
  "types": "./dist/index.d.ts",
11
11
  "import": "./dist/index.js"
12
+ },
13
+ "./client": {
14
+ "types": "./dist/client.d.ts",
15
+ "import": "./dist/client.js"
12
16
  }
13
17
  },
14
18
  "files": [