@listo-ai/mcp-observability 0.2.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 +609 -0
- package/dist/easy-setup.d.ts +34 -0
- package/dist/easy-setup.d.ts.map +1 -0
- package/dist/easy-setup.js +75 -0
- package/dist/endpoints.d.ts +34 -0
- package/dist/endpoints.d.ts.map +1 -0
- package/dist/endpoints.js +307 -0
- package/dist/index.d.ts +305 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +790 -0
- package/dist/remote-sink.d.ts +26 -0
- package/dist/remote-sink.d.ts.map +1 -0
- package/dist/remote-sink.js +123 -0
- package/package.json +63 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createMcpObservability, InMemorySink, createConsoleSink, } from './index.js';
|
|
2
|
+
import { createRemoteSink } from './remote-sink.js';
|
|
3
|
+
import { TELEMETRY_SINK_KEY, OBSERVABILITY_KEY, setGlobal, } from './endpoints.js';
|
|
4
|
+
/**
|
|
5
|
+
* Create an observability instance with environment-based defaults.
|
|
6
|
+
*
|
|
7
|
+
* This is the easiest way to get started with Listo Insights.
|
|
8
|
+
* It automatically:
|
|
9
|
+
* - Configures RemoteSink if INSIGHTS_API_KEY is provided
|
|
10
|
+
* - Configures InMemorySink + local dashboard in development
|
|
11
|
+
* - Applies sensible defaults (sampling, redaction, etc.)
|
|
12
|
+
* - Auto-detects tenant/user from common headers
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const observability = createMcpObservabilityEasy({
|
|
17
|
+
* serviceName: "my-mcp-server",
|
|
18
|
+
* serviceVersion: "1.0.0",
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
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)
|
|
25
|
+
* - NODE_ENV: Determines if dev/production mode
|
|
26
|
+
*/
|
|
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;
|
|
29
|
+
const sinks = [];
|
|
30
|
+
// Console logging in dev only
|
|
31
|
+
if (environment === 'dev') {
|
|
32
|
+
sinks.push(createConsoleSink({ logSuccess: false }));
|
|
33
|
+
}
|
|
34
|
+
// Local in-memory dashboard for development
|
|
35
|
+
if (enableLocalDashboard) {
|
|
36
|
+
const telemetrySink = new InMemorySink({ limit: 2000 });
|
|
37
|
+
sinks.push(telemetrySink);
|
|
38
|
+
// Export globally for telemetry endpoints to access
|
|
39
|
+
setGlobal(TELEMETRY_SINK_KEY, telemetrySink);
|
|
40
|
+
}
|
|
41
|
+
// Remote Listo Insights sink
|
|
42
|
+
if (insightsApiUrl && insightsApiKey) {
|
|
43
|
+
sinks.push(createRemoteSink({
|
|
44
|
+
endpoint: `${insightsApiUrl}/v1/events/batch`,
|
|
45
|
+
apiKey: insightsApiKey,
|
|
46
|
+
batchSize: 50,
|
|
47
|
+
flushIntervalMs: 5000,
|
|
48
|
+
maxRetries: 3,
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
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');
|
|
55
|
+
}
|
|
56
|
+
// Create the observability instance
|
|
57
|
+
const options = {
|
|
58
|
+
serviceName,
|
|
59
|
+
serviceVersion,
|
|
60
|
+
environment,
|
|
61
|
+
sampleRate,
|
|
62
|
+
capturePayloads: true,
|
|
63
|
+
redactKeys: ['password', 'token', 'apiKey', 'secret', 'authorization'],
|
|
64
|
+
sinks: sinks.length > 0 ? sinks : [createConsoleSink({ logSuccess: false })],
|
|
65
|
+
// Auto-detect tenant from common headers
|
|
66
|
+
tenantResolver: (req) => req.headers['x-tenant-id'] ||
|
|
67
|
+
req.headers['x-tenant-slug'] ||
|
|
68
|
+
undefined,
|
|
69
|
+
};
|
|
70
|
+
const observability = createMcpObservability(options);
|
|
71
|
+
// Export globally for telemetry router's POST /event endpoint
|
|
72
|
+
setGlobal(OBSERVABILITY_KEY, observability);
|
|
73
|
+
return observability;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=easy-setup.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import type { McpObservability } from './index.js';
|
|
3
|
+
declare const TELEMETRY_SINK_KEY: unique symbol;
|
|
4
|
+
declare const OBSERVABILITY_KEY: unique symbol;
|
|
5
|
+
declare function setGlobal<T>(key: symbol, value: T): void;
|
|
6
|
+
/**
|
|
7
|
+
* Express middleware for automatic HTTP request tracking
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { expressTelemetry } from '@listo-ai/mcp-observability';
|
|
12
|
+
*
|
|
13
|
+
* app.use(expressTelemetry(observability));
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function expressTelemetry(observability: McpObservability): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Express router for telemetry endpoints
|
|
19
|
+
*
|
|
20
|
+
* Provides:
|
|
21
|
+
* - GET /telemetry - JSON metrics
|
|
22
|
+
* - GET /telemetry/dashboard - HTML dashboard
|
|
23
|
+
* - POST /telemetry/event - UI event ingestion
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* import { createTelemetryRouter } from '@listo-ai/mcp-observability';
|
|
28
|
+
*
|
|
29
|
+
* app.use('/telemetry', createTelemetryRouter());
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare function createTelemetryRouter(): any;
|
|
33
|
+
export { TELEMETRY_SINK_KEY, OBSERVABILITY_KEY, setGlobal };
|
|
34
|
+
//# sourceMappingURL=endpoints.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
const TELEMETRY_SINK_KEY = Symbol.for('listo.telemetrySink');
|
|
2
|
+
const OBSERVABILITY_KEY = Symbol.for('listo.observability');
|
|
3
|
+
function escapeHtml(str) {
|
|
4
|
+
return str
|
|
5
|
+
.replace(/&/g, '&')
|
|
6
|
+
.replace(/</g, '<')
|
|
7
|
+
.replace(/>/g, '>')
|
|
8
|
+
.replace(/"/g, '"')
|
|
9
|
+
.replace(/'/g, ''');
|
|
10
|
+
}
|
|
11
|
+
function getGlobal(key) {
|
|
12
|
+
return globalThis[key];
|
|
13
|
+
}
|
|
14
|
+
function setGlobal(key, value) {
|
|
15
|
+
globalThis[key] = value;
|
|
16
|
+
}
|
|
17
|
+
const UI_EVENT_FIELDS = new Set([
|
|
18
|
+
'name',
|
|
19
|
+
'action',
|
|
20
|
+
'widgetId',
|
|
21
|
+
'toolName',
|
|
22
|
+
'sessionId',
|
|
23
|
+
'tenantId',
|
|
24
|
+
'locale',
|
|
25
|
+
'properties',
|
|
26
|
+
]);
|
|
27
|
+
const MAX_EVENT_BODY_SIZE = 8192;
|
|
28
|
+
function validateUiEventBody(body) {
|
|
29
|
+
if (!body || typeof body !== 'object' || Array.isArray(body))
|
|
30
|
+
return null;
|
|
31
|
+
const raw = body;
|
|
32
|
+
if (typeof raw.name !== 'string' || raw.name.length === 0)
|
|
33
|
+
return null;
|
|
34
|
+
const cleaned = {};
|
|
35
|
+
for (const key of UI_EVENT_FIELDS) {
|
|
36
|
+
if (key in raw) {
|
|
37
|
+
cleaned[key] = raw[key];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return cleaned;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Express middleware for automatic HTTP request tracking
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* import { expressTelemetry } from '@listo-ai/mcp-observability';
|
|
48
|
+
*
|
|
49
|
+
* app.use(expressTelemetry(observability));
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export function expressTelemetry(observability) {
|
|
53
|
+
return async (req, res, next) => {
|
|
54
|
+
const route = req.route?.path || req.path;
|
|
55
|
+
try {
|
|
56
|
+
await observability.trackHttpRequest(req, res, async (tracking) => {
|
|
57
|
+
tracking.setRoute(route);
|
|
58
|
+
next();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
next(error);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Express router for telemetry endpoints
|
|
68
|
+
*
|
|
69
|
+
* Provides:
|
|
70
|
+
* - GET /telemetry - JSON metrics
|
|
71
|
+
* - GET /telemetry/dashboard - HTML dashboard
|
|
72
|
+
* - POST /telemetry/event - UI event ingestion
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* import { createTelemetryRouter } from '@listo-ai/mcp-observability';
|
|
77
|
+
*
|
|
78
|
+
* app.use('/telemetry', createTelemetryRouter());
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
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
|
+
const getTelemetrySink = () => {
|
|
86
|
+
return getGlobal(TELEMETRY_SINK_KEY);
|
|
87
|
+
};
|
|
88
|
+
// JSON metrics endpoint
|
|
89
|
+
router.get('/', (req, res) => {
|
|
90
|
+
const sink = getTelemetrySink();
|
|
91
|
+
if (!sink) {
|
|
92
|
+
return res.status(404).json({
|
|
93
|
+
error: 'Local telemetry not enabled',
|
|
94
|
+
hint: 'Set enableLocalDashboard: true in createMcpObservabilityEasy config',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
res.json({
|
|
98
|
+
events: sink.getEvents(),
|
|
99
|
+
metrics: sink.getMetrics(),
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
// HTML dashboard endpoint
|
|
103
|
+
router.get('/dashboard', (req, res) => {
|
|
104
|
+
const sink = getTelemetrySink();
|
|
105
|
+
if (!sink) {
|
|
106
|
+
return res
|
|
107
|
+
.status(404)
|
|
108
|
+
.type('text/html')
|
|
109
|
+
.set('Content-Security-Policy', "default-src 'none'; style-src 'unsafe-inline'").send(`
|
|
110
|
+
<!DOCTYPE html>
|
|
111
|
+
<html>
|
|
112
|
+
<head>
|
|
113
|
+
<title>Listo Telemetry</title>
|
|
114
|
+
<style>
|
|
115
|
+
body { font-family: system-ui, sans-serif; padding: 20px; }
|
|
116
|
+
.error { color: #d32f2f; }
|
|
117
|
+
</style>
|
|
118
|
+
</head>
|
|
119
|
+
<body>
|
|
120
|
+
<h1>Listo Telemetry Dashboard</h1>
|
|
121
|
+
<p class="error">Local telemetry is not enabled.</p>
|
|
122
|
+
<p>To enable, set enableLocalDashboard: true in createMcpObservabilityEasy config.</p>
|
|
123
|
+
</body>
|
|
124
|
+
</html>
|
|
125
|
+
`);
|
|
126
|
+
}
|
|
127
|
+
const metrics = sink.getMetrics();
|
|
128
|
+
const html = `
|
|
129
|
+
<!DOCTYPE html>
|
|
130
|
+
<html>
|
|
131
|
+
<head>
|
|
132
|
+
<meta charset="UTF-8">
|
|
133
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
134
|
+
<title>Listo Telemetry Dashboard</title>
|
|
135
|
+
<style>
|
|
136
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
137
|
+
body {
|
|
138
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
139
|
+
background: #f5f5f5;
|
|
140
|
+
color: #333;
|
|
141
|
+
}
|
|
142
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
|
143
|
+
h1 { font-size: 28px; margin-bottom: 20px; }
|
|
144
|
+
h2 { font-size: 18px; margin-top: 30px; margin-bottom: 10px; }
|
|
145
|
+
.grid {
|
|
146
|
+
display: grid;
|
|
147
|
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
148
|
+
gap: 15px;
|
|
149
|
+
margin-bottom: 20px;
|
|
150
|
+
}
|
|
151
|
+
.card {
|
|
152
|
+
background: white;
|
|
153
|
+
border-radius: 8px;
|
|
154
|
+
padding: 20px;
|
|
155
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
156
|
+
}
|
|
157
|
+
.metric-value { font-size: 32px; font-weight: bold; margin-top: 10px; }
|
|
158
|
+
.metric-label { font-size: 12px; color: #666; text-transform: uppercase; }
|
|
159
|
+
table {
|
|
160
|
+
width: 100%;
|
|
161
|
+
border-collapse: collapse;
|
|
162
|
+
background: white;
|
|
163
|
+
border-radius: 8px;
|
|
164
|
+
overflow: hidden;
|
|
165
|
+
}
|
|
166
|
+
th, td { padding: 12px; text-align: left; }
|
|
167
|
+
th { background: #f5f5f5; font-weight: 600; }
|
|
168
|
+
tr:hover { background: #fafafa; }
|
|
169
|
+
.refresh { margin-bottom: 20px; }
|
|
170
|
+
button {
|
|
171
|
+
padding: 10px 15px;
|
|
172
|
+
background: #1976d2;
|
|
173
|
+
color: white;
|
|
174
|
+
border: none;
|
|
175
|
+
border-radius: 4px;
|
|
176
|
+
cursor: pointer;
|
|
177
|
+
font-size: 14px;
|
|
178
|
+
}
|
|
179
|
+
button:hover { background: #1565c0; }
|
|
180
|
+
</style>
|
|
181
|
+
</head>
|
|
182
|
+
<body>
|
|
183
|
+
<div class="container">
|
|
184
|
+
<div class="refresh">
|
|
185
|
+
<button onclick="location.reload()">⟲ Refresh</button>
|
|
186
|
+
<small style="margin-left: 10px; color: #666;">Auto-refreshes every 5 seconds</small>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<h1>📊 Listo Telemetry Dashboard</h1>
|
|
190
|
+
|
|
191
|
+
<div class="grid">
|
|
192
|
+
<div class="card">
|
|
193
|
+
<div class="metric-label">Total Events</div>
|
|
194
|
+
<div class="metric-value">${metrics.totals.events}</div>
|
|
195
|
+
</div>
|
|
196
|
+
<div class="card">
|
|
197
|
+
<div class="metric-label">HTTP Requests</div>
|
|
198
|
+
<div class="metric-value">${metrics.totals.http}</div>
|
|
199
|
+
</div>
|
|
200
|
+
<div class="card">
|
|
201
|
+
<div class="metric-label">MCP Requests</div>
|
|
202
|
+
<div class="metric-value">${metrics.totals.mcp}</div>
|
|
203
|
+
</div>
|
|
204
|
+
<div class="card">
|
|
205
|
+
<div class="metric-label">Sessions</div>
|
|
206
|
+
<div class="metric-value">${metrics.totals.sessions}</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
${metrics.http.overall.count > 0
|
|
211
|
+
? `
|
|
212
|
+
<h2>HTTP Performance</h2>
|
|
213
|
+
<table>
|
|
214
|
+
<tr>
|
|
215
|
+
<th>Route</th>
|
|
216
|
+
<th>Requests</th>
|
|
217
|
+
<th>Errors</th>
|
|
218
|
+
<th>Avg (ms)</th>
|
|
219
|
+
<th>P95 (ms)</th>
|
|
220
|
+
<th>P99 (ms)</th>
|
|
221
|
+
</tr>
|
|
222
|
+
${Object.entries(metrics.http.byRoute)
|
|
223
|
+
.map(([route, stats]) => {
|
|
224
|
+
const s = stats;
|
|
225
|
+
return `
|
|
226
|
+
<tr>
|
|
227
|
+
<td><code>${escapeHtml(route)}</code></td>
|
|
228
|
+
<td>${s.count}</td>
|
|
229
|
+
<td style="color: ${s.errors > 0 ? '#d32f2f' : '#4caf50'}">${s.errors}</td>
|
|
230
|
+
<td>${(s.avgMs || 0).toFixed(1)}</td>
|
|
231
|
+
<td>${(s.p95 || 0).toFixed(1)}</td>
|
|
232
|
+
<td>${(s.p99 || 0).toFixed(1)}</td>
|
|
233
|
+
</tr>
|
|
234
|
+
`;
|
|
235
|
+
})
|
|
236
|
+
.join('')}
|
|
237
|
+
</table>
|
|
238
|
+
`
|
|
239
|
+
: ''}
|
|
240
|
+
|
|
241
|
+
${metrics.mcp.tools && Object.keys(metrics.mcp.tools).length > 0
|
|
242
|
+
? `
|
|
243
|
+
<h2>MCP Tools</h2>
|
|
244
|
+
<table>
|
|
245
|
+
<tr>
|
|
246
|
+
<th>Tool</th>
|
|
247
|
+
<th>Calls</th>
|
|
248
|
+
<th>Errors</th>
|
|
249
|
+
<th>Avg (ms)</th>
|
|
250
|
+
<th>P95 (ms)</th>
|
|
251
|
+
<th>P99 (ms)</th>
|
|
252
|
+
</tr>
|
|
253
|
+
${Object.entries(metrics.mcp.tools)
|
|
254
|
+
.map(([tool, stats]) => {
|
|
255
|
+
const s = stats;
|
|
256
|
+
return `
|
|
257
|
+
<tr>
|
|
258
|
+
<td><code>${escapeHtml(tool)}</code></td>
|
|
259
|
+
<td>${s.count}</td>
|
|
260
|
+
<td style="color: ${s.errors > 0 ? '#d32f2f' : '#4caf50'}">${s.errors}</td>
|
|
261
|
+
<td>${(s.avgMs || 0).toFixed(1)}</td>
|
|
262
|
+
<td>${(s.p95 || 0).toFixed(1)}</td>
|
|
263
|
+
<td>${(s.p99 || 0).toFixed(1)}</td>
|
|
264
|
+
</tr>
|
|
265
|
+
`;
|
|
266
|
+
})
|
|
267
|
+
.join('')}
|
|
268
|
+
</table>
|
|
269
|
+
`
|
|
270
|
+
: ''}
|
|
271
|
+
|
|
272
|
+
<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>
|
|
275
|
+
</p>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<script>
|
|
279
|
+
setTimeout(() => location.reload(), 5000);
|
|
280
|
+
</script>
|
|
281
|
+
</body>
|
|
282
|
+
</html>
|
|
283
|
+
`;
|
|
284
|
+
res
|
|
285
|
+
.type('text/html')
|
|
286
|
+
.set('Content-Security-Policy', "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'")
|
|
287
|
+
.send(html);
|
|
288
|
+
});
|
|
289
|
+
// UI event ingestion endpoint
|
|
290
|
+
router.post('/event', express.json({ limit: MAX_EVENT_BODY_SIZE }), (req, res) => {
|
|
291
|
+
const validated = validateUiEventBody(req.body);
|
|
292
|
+
if (!validated) {
|
|
293
|
+
res
|
|
294
|
+
.status(400)
|
|
295
|
+
.json({ error: 'Invalid event: "name" string required' });
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const observability = getGlobal(OBSERVABILITY_KEY);
|
|
299
|
+
if (observability) {
|
|
300
|
+
observability.recordUiEvent(validated);
|
|
301
|
+
}
|
|
302
|
+
res.status(201).json({ ok: true });
|
|
303
|
+
});
|
|
304
|
+
return router;
|
|
305
|
+
}
|
|
306
|
+
export { TELEMETRY_SINK_KEY, OBSERVABILITY_KEY, setGlobal };
|
|
307
|
+
//# sourceMappingURL=endpoints.js.map
|