within-sdk 1.0.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/LICENSE +1 -0
- package/README.md +77 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +243 -0
- package/dist/middleware.d.ts +40 -0
- package/dist/middleware.js +562 -0
- package/dist/modules/compatibility.d.ts +18 -0
- package/dist/modules/compatibility.js +99 -0
- package/dist/modules/constants.d.ts +12 -0
- package/dist/modules/constants.js +12 -0
- package/dist/modules/context-parameters.d.ts +13 -0
- package/dist/modules/context-parameters.js +74 -0
- package/dist/modules/diagnostics.d.ts +7 -0
- package/dist/modules/diagnostics.js +12 -0
- package/dist/modules/eventQueue.d.ts +32 -0
- package/dist/modules/eventQueue.js +234 -0
- package/dist/modules/exceptions.d.ts +13 -0
- package/dist/modules/exceptions.js +730 -0
- package/dist/modules/exporters/datadog.d.ts +17 -0
- package/dist/modules/exporters/datadog.js +166 -0
- package/dist/modules/exporters/otlp.d.ts +13 -0
- package/dist/modules/exporters/otlp.js +140 -0
- package/dist/modules/exporters/posthog.d.ts +32 -0
- package/dist/modules/exporters/posthog.js +272 -0
- package/dist/modules/exporters/sentry.d.ts +27 -0
- package/dist/modules/exporters/sentry.js +386 -0
- package/dist/modules/exporters/trace-context.d.ts +8 -0
- package/dist/modules/exporters/trace-context.js +27 -0
- package/dist/modules/index.d.ts +8 -0
- package/dist/modules/index.js +8 -0
- package/dist/modules/internal.d.ts +33 -0
- package/dist/modules/internal.js +195 -0
- package/dist/modules/logging.d.ts +2 -0
- package/dist/modules/logging.js +74 -0
- package/dist/modules/mcp-sdk-compat.d.ts +32 -0
- package/dist/modules/mcp-sdk-compat.js +111 -0
- package/dist/modules/privacy.d.ts +15 -0
- package/dist/modules/privacy.js +179 -0
- package/dist/modules/redaction.d.ts +11 -0
- package/dist/modules/redaction.js +81 -0
- package/dist/modules/sanitization.d.ts +9 -0
- package/dist/modules/sanitization.js +111 -0
- package/dist/modules/session.d.ts +22 -0
- package/dist/modules/session.js +109 -0
- package/dist/modules/telemetry.d.ts +8 -0
- package/dist/modules/telemetry.js +53 -0
- package/dist/modules/tools.d.ts +25 -0
- package/dist/modules/tools.js +119 -0
- package/dist/modules/tracing.d.ts +4 -0
- package/dist/modules/tracing.js +263 -0
- package/dist/modules/tracingV2.d.ts +2 -0
- package/dist/modules/tracingV2.js +300 -0
- package/dist/modules/truncation.d.ts +24 -0
- package/dist/modules/truncation.js +303 -0
- package/dist/modules/validation.d.ts +6 -0
- package/dist/modules/validation.js +51 -0
- package/dist/network.d.ts +83 -0
- package/dist/network.js +411 -0
- package/dist/proxy.d.ts +31 -0
- package/dist/proxy.js +158 -0
- package/dist/thirdparty/ksuid/base-convert-int-array.js +49 -0
- package/dist/thirdparty/ksuid/base62.js +24 -0
- package/dist/thirdparty/ksuid/index.d.ts +30 -0
- package/dist/thirdparty/ksuid/index.js +201 -0
- package/dist/types.d.ts +197 -0
- package/dist/types.js +5 -0
- package/dist/validate.d.ts +53 -0
- package/dist/validate.js +139 -0
- package/package.json +43 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { writeToLog } from "../logging.js";
|
|
2
|
+
import { traceContext } from "./trace-context.js";
|
|
3
|
+
import { WITHIN_SOURCE } from "../constants.js";
|
|
4
|
+
export class SentryExporter {
|
|
5
|
+
endpoint;
|
|
6
|
+
authHeader;
|
|
7
|
+
config;
|
|
8
|
+
parsedDSN;
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.parsedDSN = this.parseDSN(config.dsn);
|
|
12
|
+
// Build envelope endpoint
|
|
13
|
+
this.endpoint = `${this.parsedDSN.protocol}://${this.parsedDSN.host}${this.parsedDSN.port ? `:${this.parsedDSN.port}` : ""}${this.parsedDSN.path}/api/${this.parsedDSN.projectId}/envelope/`;
|
|
14
|
+
// Build auth header
|
|
15
|
+
this.authHeader = `Sentry sentry_version=7, sentry_client=within-sdk/1.0.2, sentry_key=${this.parsedDSN.publicKey}`;
|
|
16
|
+
writeToLog(`SentryExporter: Initialized with endpoint ${this.endpoint}`);
|
|
17
|
+
}
|
|
18
|
+
parseDSN(dsn) {
|
|
19
|
+
// DSN format: protocol://publicKey@host[:port]/path/projectId
|
|
20
|
+
const regex = /^(https?):\/\/([a-f0-9]+)@([\w.-]+)(:\d+)?(\/.*)?\/(\d+)$/;
|
|
21
|
+
const match = dsn.match(regex);
|
|
22
|
+
if (!match) {
|
|
23
|
+
throw new Error(`Invalid Sentry DSN: ${dsn}`);
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
protocol: match[1],
|
|
27
|
+
publicKey: match[2],
|
|
28
|
+
host: match[3],
|
|
29
|
+
port: match[4]?.substring(1), // Remove leading ':'
|
|
30
|
+
path: match[5] || "",
|
|
31
|
+
projectId: match[6],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async export(event) {
|
|
35
|
+
try {
|
|
36
|
+
// ALWAYS send log
|
|
37
|
+
const log = this.eventToLog(event);
|
|
38
|
+
const logEnvelope = this.createLogEnvelope(log);
|
|
39
|
+
writeToLog(`SentryExporter: Sending log for event ${event.id} to Sentry`);
|
|
40
|
+
const logResponse = await fetch(this.endpoint, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: {
|
|
43
|
+
"X-Sentry-Auth": this.authHeader,
|
|
44
|
+
"Content-Type": "application/x-sentry-envelope",
|
|
45
|
+
},
|
|
46
|
+
body: logEnvelope,
|
|
47
|
+
});
|
|
48
|
+
if (!logResponse.ok) {
|
|
49
|
+
const errorBody = await logResponse.text();
|
|
50
|
+
writeToLog(`Sentry log export failed - Status: ${logResponse.status}, Body: ${errorBody}`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
writeToLog(`Sentry log export success - Event: ${event.id}`);
|
|
54
|
+
}
|
|
55
|
+
// OPTIONALLY send transaction for performance monitoring
|
|
56
|
+
if (this.config.enableTracing) {
|
|
57
|
+
const transaction = this.eventToTransaction(event);
|
|
58
|
+
const transactionEnvelope = this.createTransactionEnvelope(transaction);
|
|
59
|
+
writeToLog(`SentryExporter: Sending transaction ${transaction.event_id} to Sentry`);
|
|
60
|
+
const transactionResponse = await fetch(this.endpoint, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: {
|
|
63
|
+
"X-Sentry-Auth": this.authHeader,
|
|
64
|
+
"Content-Type": "application/x-sentry-envelope",
|
|
65
|
+
},
|
|
66
|
+
body: transactionEnvelope,
|
|
67
|
+
});
|
|
68
|
+
if (!transactionResponse.ok) {
|
|
69
|
+
const errorBody = await transactionResponse.text();
|
|
70
|
+
writeToLog(`Sentry transaction export failed - Status: ${transactionResponse.status}, Body: ${errorBody}`);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
writeToLog(`Sentry transaction export success - Event: ${event.id}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// ALWAYS send error event for Issue creation if this is an error
|
|
77
|
+
if (event.isError) {
|
|
78
|
+
// Use transaction if available for better context, otherwise create minimal error event
|
|
79
|
+
const errorEvent = this.config.enableTracing
|
|
80
|
+
? this.eventToErrorEvent(event, this.eventToTransaction(event))
|
|
81
|
+
: this.eventToErrorEvent(event);
|
|
82
|
+
const errorEnvelope = this.createErrorEnvelope(errorEvent);
|
|
83
|
+
writeToLog(`SentryExporter: Sending error event ${errorEvent.event_id} to Sentry for Issue creation`);
|
|
84
|
+
const errorResponse = await fetch(this.endpoint, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: {
|
|
87
|
+
"X-Sentry-Auth": this.authHeader,
|
|
88
|
+
"Content-Type": "application/x-sentry-envelope",
|
|
89
|
+
},
|
|
90
|
+
body: errorEnvelope,
|
|
91
|
+
});
|
|
92
|
+
if (!errorResponse.ok) {
|
|
93
|
+
const errorBody = await errorResponse.text();
|
|
94
|
+
writeToLog(`Sentry error export failed - Status: ${errorResponse.status}, Body: ${errorBody}`);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
writeToLog(`Sentry error export success - Event: ${event.id}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
writeToLog(`Sentry export error: ${error}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
eventToLog(event) {
|
|
106
|
+
const timestamp = event.timestamp
|
|
107
|
+
? new Date(event.timestamp).getTime() / 1000
|
|
108
|
+
: Date.now() / 1000;
|
|
109
|
+
const traceId = traceContext.getTraceId(event.sessionId);
|
|
110
|
+
// Generate deterministic event_id for Sentry
|
|
111
|
+
const eventId = traceContext.getSpanId(event.id) + traceContext.getSpanId(event.id);
|
|
112
|
+
// Build message
|
|
113
|
+
const message = event.resourceName
|
|
114
|
+
? `MCP ${event.eventType || "event"}: ${event.resourceName}`
|
|
115
|
+
: `MCP ${event.eventType || "event"}`;
|
|
116
|
+
return {
|
|
117
|
+
timestamp,
|
|
118
|
+
trace_id: traceId,
|
|
119
|
+
event_id: eventId,
|
|
120
|
+
level: event.isError ? "error" : "info",
|
|
121
|
+
body: message,
|
|
122
|
+
attributes: this.buildLogAttributes(event),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
buildLogAttributes(event) {
|
|
126
|
+
const attributes = {};
|
|
127
|
+
if (event.eventType) {
|
|
128
|
+
attributes.eventType = { value: event.eventType, type: "string" };
|
|
129
|
+
}
|
|
130
|
+
if (event.resourceName) {
|
|
131
|
+
attributes.resourceName = { value: event.resourceName, type: "string" };
|
|
132
|
+
}
|
|
133
|
+
if (event.serverName) {
|
|
134
|
+
attributes.serverName = { value: event.serverName, type: "string" };
|
|
135
|
+
}
|
|
136
|
+
if (event.clientName) {
|
|
137
|
+
attributes.clientName = { value: event.clientName, type: "string" };
|
|
138
|
+
}
|
|
139
|
+
if (event.sessionId) {
|
|
140
|
+
attributes.sessionId = { value: event.sessionId, type: "string" };
|
|
141
|
+
}
|
|
142
|
+
if (event.projectId) {
|
|
143
|
+
attributes.projectId = { value: event.projectId, type: "string" };
|
|
144
|
+
}
|
|
145
|
+
if (event.duration !== undefined) {
|
|
146
|
+
attributes.duration_ms = { value: event.duration, type: "double" };
|
|
147
|
+
}
|
|
148
|
+
if (event.identifyActorGivenId) {
|
|
149
|
+
attributes.actorId = {
|
|
150
|
+
value: event.identifyActorGivenId,
|
|
151
|
+
type: "string",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (event.identifyActorName) {
|
|
155
|
+
attributes.actorName = { value: event.identifyActorName, type: "string" };
|
|
156
|
+
}
|
|
157
|
+
if (event.userIntent) {
|
|
158
|
+
attributes.userIntent = { value: event.userIntent, type: "string" };
|
|
159
|
+
}
|
|
160
|
+
if (event.serverVersion) {
|
|
161
|
+
attributes.serverVersion = { value: event.serverVersion, type: "string" };
|
|
162
|
+
}
|
|
163
|
+
if (event.clientVersion) {
|
|
164
|
+
attributes.clientVersion = { value: event.clientVersion, type: "string" };
|
|
165
|
+
}
|
|
166
|
+
if (event.isError !== undefined) {
|
|
167
|
+
attributes.isError = { value: event.isError, type: "boolean" };
|
|
168
|
+
}
|
|
169
|
+
return attributes;
|
|
170
|
+
}
|
|
171
|
+
createLogEnvelope(log) {
|
|
172
|
+
// Envelope header
|
|
173
|
+
const envelopeHeader = {
|
|
174
|
+
event_id: log.event_id,
|
|
175
|
+
sent_at: new Date().toISOString(),
|
|
176
|
+
};
|
|
177
|
+
// Item header with ALL MANDATORY fields
|
|
178
|
+
const itemHeader = {
|
|
179
|
+
type: "log",
|
|
180
|
+
item_count: 1, // MANDATORY - must match number of logs
|
|
181
|
+
content_type: "application/vnd.sentry.items.log+json", // MANDATORY - exact string
|
|
182
|
+
};
|
|
183
|
+
// Payload with CORRECT key
|
|
184
|
+
const payload = {
|
|
185
|
+
items: [log], // Changed from 'logs' to 'items'
|
|
186
|
+
};
|
|
187
|
+
// Build envelope with TRAILING NEWLINE
|
|
188
|
+
return ([
|
|
189
|
+
JSON.stringify(envelopeHeader),
|
|
190
|
+
JSON.stringify(itemHeader),
|
|
191
|
+
JSON.stringify(payload),
|
|
192
|
+
].join("\n") + "\n"); // Added required trailing newline
|
|
193
|
+
}
|
|
194
|
+
eventToTransaction(event) {
|
|
195
|
+
// Calculate timestamps
|
|
196
|
+
const endTimestamp = event.timestamp
|
|
197
|
+
? new Date(event.timestamp).getTime() / 1000
|
|
198
|
+
: Date.now() / 1000;
|
|
199
|
+
const startTimestamp = event.duration
|
|
200
|
+
? endTimestamp - event.duration / 1000
|
|
201
|
+
: endTimestamp;
|
|
202
|
+
const traceId = traceContext.getTraceId(event.sessionId);
|
|
203
|
+
const spanId = traceContext.getSpanId(event.id);
|
|
204
|
+
// Build transaction name
|
|
205
|
+
const transactionName = event.resourceName
|
|
206
|
+
? `${event.eventType || "mcp"} - ${event.resourceName}`
|
|
207
|
+
: event.eventType || "mcp.event";
|
|
208
|
+
const transaction = {
|
|
209
|
+
type: "transaction",
|
|
210
|
+
event_id: traceContext.getSpanId(event.id) + traceContext.getSpanId(),
|
|
211
|
+
timestamp: endTimestamp,
|
|
212
|
+
start_timestamp: startTimestamp,
|
|
213
|
+
transaction: transactionName,
|
|
214
|
+
contexts: this.buildContexts(event, {
|
|
215
|
+
trace_id: traceId,
|
|
216
|
+
span_id: spanId,
|
|
217
|
+
op: event.eventType || "mcp.event",
|
|
218
|
+
status: event.isError ? "internal_error" : "ok",
|
|
219
|
+
}),
|
|
220
|
+
tags: this.buildTags(event),
|
|
221
|
+
extra: this.buildExtra(event),
|
|
222
|
+
};
|
|
223
|
+
return transaction;
|
|
224
|
+
}
|
|
225
|
+
buildTags(event) {
|
|
226
|
+
const tags = {
|
|
227
|
+
source: WITHIN_SOURCE,
|
|
228
|
+
};
|
|
229
|
+
if (this.config.environment)
|
|
230
|
+
tags.environment = this.config.environment;
|
|
231
|
+
if (this.config.release)
|
|
232
|
+
tags.release = this.config.release;
|
|
233
|
+
if (event.eventType)
|
|
234
|
+
tags.event_type = event.eventType;
|
|
235
|
+
if (event.resourceName)
|
|
236
|
+
tags.resource = event.resourceName;
|
|
237
|
+
if (event.serverName)
|
|
238
|
+
tags.server_name = event.serverName;
|
|
239
|
+
if (event.clientName)
|
|
240
|
+
tags.client_name = event.clientName;
|
|
241
|
+
if (event.identifyActorGivenId)
|
|
242
|
+
tags.actor_id = event.identifyActorGivenId;
|
|
243
|
+
// Add customer-defined tags (namespaced to avoid collisions with Sentry reserved fields)
|
|
244
|
+
if (event.tags) {
|
|
245
|
+
for (const [key, value] of Object.entries(event.tags)) {
|
|
246
|
+
tags[`within.${key}`] = value;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return tags;
|
|
250
|
+
}
|
|
251
|
+
buildExtra(event) {
|
|
252
|
+
const extra = {};
|
|
253
|
+
if (event.sessionId)
|
|
254
|
+
extra.session_id = event.sessionId;
|
|
255
|
+
if (event.projectId)
|
|
256
|
+
extra.project_id = event.projectId;
|
|
257
|
+
if (event.userIntent)
|
|
258
|
+
extra.user_intent = event.userIntent;
|
|
259
|
+
if (event.identifyActorName)
|
|
260
|
+
extra.actor_name = event.identifyActorName;
|
|
261
|
+
if (event.serverVersion)
|
|
262
|
+
extra.server_version = event.serverVersion;
|
|
263
|
+
if (event.clientVersion)
|
|
264
|
+
extra.client_version = event.clientVersion;
|
|
265
|
+
if (event.duration !== undefined)
|
|
266
|
+
extra.duration_ms = event.duration;
|
|
267
|
+
if (event.error)
|
|
268
|
+
extra.error = event.error;
|
|
269
|
+
return extra;
|
|
270
|
+
}
|
|
271
|
+
buildContexts(event, traceCtx) {
|
|
272
|
+
const contexts = {
|
|
273
|
+
trace: traceCtx,
|
|
274
|
+
};
|
|
275
|
+
// Add customer-defined properties as a custom context
|
|
276
|
+
if (event.properties) {
|
|
277
|
+
contexts.within = event.properties;
|
|
278
|
+
}
|
|
279
|
+
return contexts;
|
|
280
|
+
}
|
|
281
|
+
eventToErrorEvent(event, transaction) {
|
|
282
|
+
// Extract error message
|
|
283
|
+
let errorMessage = "Unknown error";
|
|
284
|
+
let errorType = "ToolCallError";
|
|
285
|
+
if (event.error) {
|
|
286
|
+
if (typeof event.error === "string") {
|
|
287
|
+
errorMessage = event.error;
|
|
288
|
+
}
|
|
289
|
+
else if (typeof event.error === "object" && event.error !== null) {
|
|
290
|
+
if ("message" in event.error) {
|
|
291
|
+
errorMessage = String(event.error.message);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
errorMessage = JSON.stringify(event.error);
|
|
295
|
+
}
|
|
296
|
+
if ("type" in event.error) {
|
|
297
|
+
errorType = String(event.error.type);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// Use same trace context as the transaction for correlation (if available)
|
|
302
|
+
const traceId = transaction
|
|
303
|
+
? transaction.contexts.trace.trace_id
|
|
304
|
+
: traceContext.getTraceId(event.sessionId);
|
|
305
|
+
const spanId = traceContext.getSpanId(event.id);
|
|
306
|
+
const timestamp = transaction
|
|
307
|
+
? transaction.timestamp
|
|
308
|
+
: event.timestamp
|
|
309
|
+
? new Date(event.timestamp).getTime() / 1000
|
|
310
|
+
: Date.now() / 1000;
|
|
311
|
+
const errorEvent = {
|
|
312
|
+
type: "event",
|
|
313
|
+
event_id: traceContext.getSpanId(event.id) + traceContext.getSpanId(),
|
|
314
|
+
timestamp,
|
|
315
|
+
level: "error",
|
|
316
|
+
exception: {
|
|
317
|
+
values: [
|
|
318
|
+
{
|
|
319
|
+
type: errorType,
|
|
320
|
+
value: errorMessage,
|
|
321
|
+
mechanism: {
|
|
322
|
+
type: "mcp_tool_call",
|
|
323
|
+
handled: false,
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
],
|
|
327
|
+
},
|
|
328
|
+
contexts: {
|
|
329
|
+
...this.buildContexts(event, {
|
|
330
|
+
trace_id: traceId,
|
|
331
|
+
span_id: spanId,
|
|
332
|
+
parent_span_id: transaction?.contexts.trace.span_id,
|
|
333
|
+
op: transaction?.contexts.trace.op || event.eventType || "mcp.event",
|
|
334
|
+
}),
|
|
335
|
+
mcp: {
|
|
336
|
+
resource_name: event.resourceName,
|
|
337
|
+
session_id: event.sessionId,
|
|
338
|
+
event_type: event.eventType,
|
|
339
|
+
user_intent: event.userIntent,
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
tags: this.buildTags(event),
|
|
343
|
+
extra: this.buildExtra(event),
|
|
344
|
+
transaction: transaction?.transaction ||
|
|
345
|
+
(event.resourceName
|
|
346
|
+
? `${event.eventType || "mcp"} - ${event.resourceName}`
|
|
347
|
+
: event.eventType || "mcp.event"), // Generate transaction name if not available
|
|
348
|
+
};
|
|
349
|
+
return errorEvent;
|
|
350
|
+
}
|
|
351
|
+
createTransactionEnvelope(transaction) {
|
|
352
|
+
// Envelope header
|
|
353
|
+
const envelopeHeader = {
|
|
354
|
+
event_id: transaction.event_id,
|
|
355
|
+
sent_at: new Date().toISOString(),
|
|
356
|
+
};
|
|
357
|
+
// Item header for transaction
|
|
358
|
+
const itemHeader = {
|
|
359
|
+
type: "transaction",
|
|
360
|
+
};
|
|
361
|
+
// Build envelope (newline-separated JSON)
|
|
362
|
+
return [
|
|
363
|
+
JSON.stringify(envelopeHeader),
|
|
364
|
+
JSON.stringify(itemHeader),
|
|
365
|
+
JSON.stringify(transaction),
|
|
366
|
+
].join("\n");
|
|
367
|
+
}
|
|
368
|
+
createErrorEnvelope(errorEvent) {
|
|
369
|
+
// Envelope header
|
|
370
|
+
const envelopeHeader = {
|
|
371
|
+
event_id: errorEvent.event_id,
|
|
372
|
+
sent_at: new Date().toISOString(),
|
|
373
|
+
};
|
|
374
|
+
// Item header for error event
|
|
375
|
+
const itemHeader = {
|
|
376
|
+
type: "event",
|
|
377
|
+
content_type: "application/json",
|
|
378
|
+
};
|
|
379
|
+
// Build envelope (newline-separated JSON)
|
|
380
|
+
return [
|
|
381
|
+
JSON.stringify(envelopeHeader),
|
|
382
|
+
JSON.stringify(itemHeader),
|
|
383
|
+
JSON.stringify(errorEvent),
|
|
384
|
+
].join("\n");
|
|
385
|
+
}
|
|
386
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
declare class TraceContext {
|
|
2
|
+
getTraceId(sessionId?: string): string;
|
|
3
|
+
getSpanId(eventId?: string): string;
|
|
4
|
+
getDatadogTraceId(sessionId?: string): string;
|
|
5
|
+
getDatadogSpanId(eventId?: string): string;
|
|
6
|
+
}
|
|
7
|
+
export declare const traceContext: TraceContext;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "crypto";
|
|
2
|
+
class TraceContext {
|
|
3
|
+
getTraceId(sessionId) {
|
|
4
|
+
if (!sessionId) {
|
|
5
|
+
return randomBytes(16).toString("hex");
|
|
6
|
+
}
|
|
7
|
+
return createHash("sha256")
|
|
8
|
+
.update(sessionId)
|
|
9
|
+
.digest("hex")
|
|
10
|
+
.substring(0, 32);
|
|
11
|
+
}
|
|
12
|
+
getSpanId(eventId) {
|
|
13
|
+
if (!eventId) {
|
|
14
|
+
return randomBytes(8).toString("hex");
|
|
15
|
+
}
|
|
16
|
+
return createHash("sha256").update(eventId).digest("hex").substring(0, 16);
|
|
17
|
+
}
|
|
18
|
+
getDatadogTraceId(sessionId) {
|
|
19
|
+
const hex = this.getTraceId(sessionId);
|
|
20
|
+
return BigInt("0x" + hex.substring(16, 32)).toString();
|
|
21
|
+
}
|
|
22
|
+
getDatadogSpanId(eventId) {
|
|
23
|
+
const hex = this.getSpanId(eventId);
|
|
24
|
+
return BigInt("0x" + hex).toString();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export const traceContext = new TraceContext();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./compatibility.js";
|
|
2
|
+
export * from "./context-parameters.js";
|
|
3
|
+
export * from "./exceptions.js";
|
|
4
|
+
export * from "./internal.js";
|
|
5
|
+
export * from "./logging.js";
|
|
6
|
+
export * from "./session.js";
|
|
7
|
+
export * from "./tools.js";
|
|
8
|
+
export * from "./tracing.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./compatibility.js";
|
|
2
|
+
export * from "./context-parameters.js";
|
|
3
|
+
export * from "./exceptions.js";
|
|
4
|
+
export * from "./internal.js";
|
|
5
|
+
export * from "./logging.js";
|
|
6
|
+
export * from "./session.js";
|
|
7
|
+
export * from "./tools.js";
|
|
8
|
+
export * from "./tracing.js";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { WithinTrackingData, MCPServerLike, UserIdentity, CompatibleRequestHandlerExtra } from "../types.js";
|
|
2
|
+
export declare function getServerTrackingData(server: MCPServerLike): WithinTrackingData | undefined;
|
|
3
|
+
export declare function setServerTrackingData(server: MCPServerLike, data: WithinTrackingData): void;
|
|
4
|
+
/**
|
|
5
|
+
* Deep comparison of two UserIdentity objects
|
|
6
|
+
*/
|
|
7
|
+
export declare function areIdentitiesEqual(a: UserIdentity, b: UserIdentity): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Merges two UserIdentity objects, overwriting userId and userName,
|
|
10
|
+
* but merging userData fields
|
|
11
|
+
*/
|
|
12
|
+
export declare function mergeIdentities(previous: UserIdentity | undefined, next: UserIdentity): UserIdentity;
|
|
13
|
+
/**
|
|
14
|
+
* Handles user identification for a request.
|
|
15
|
+
* Calls the identify function if configured, compares with previous identity,
|
|
16
|
+
* and publishes an identify event only if the identity has changed.
|
|
17
|
+
*
|
|
18
|
+
* @param server - The MCP server instance
|
|
19
|
+
* @param data - The server tracking data
|
|
20
|
+
* @param request - The request object to pass to identify function
|
|
21
|
+
* @param extra - Optional extra parameters containing headers, sessionId, etc.
|
|
22
|
+
*/
|
|
23
|
+
export declare function handleIdentify(server: MCPServerLike, data: WithinTrackingData, request: any, extra?: CompatibleRequestHandlerExtra): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Resolves the eventTags callback, validates the result, and returns validated tags.
|
|
26
|
+
* Returns null if no callback configured, callback returns nullish, or callback throws.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveEventTags(data: WithinTrackingData, request: any, extra?: CompatibleRequestHandlerExtra): Promise<Record<string, string> | null>;
|
|
29
|
+
/**
|
|
30
|
+
* Resolves the eventProperties callback and returns the result.
|
|
31
|
+
* Returns null if no callback configured, callback returns nullish, or callback throws.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveEventProperties(data: WithinTrackingData, request: any, extra?: CompatibleRequestHandlerExtra): Promise<Record<string, any> | null>;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { publishEvent } from "./eventQueue.js";
|
|
2
|
+
import { writeToLog } from "./logging.js";
|
|
3
|
+
import { validateTags } from "./validation.js";
|
|
4
|
+
import { WithinEventType } from "./constants.js";
|
|
5
|
+
import { sanitizeIdentity } from "./privacy.js";
|
|
6
|
+
/**
|
|
7
|
+
* Simple LRU cache for session identities.
|
|
8
|
+
* Prevents memory leaks by capping at maxSize entries.
|
|
9
|
+
* This cache persists across server instance restarts.
|
|
10
|
+
*/
|
|
11
|
+
class IdentityCache {
|
|
12
|
+
cache;
|
|
13
|
+
maxSize;
|
|
14
|
+
constructor(maxSize = 1000) {
|
|
15
|
+
this.cache = new Map();
|
|
16
|
+
this.maxSize = maxSize;
|
|
17
|
+
}
|
|
18
|
+
get(sessionId) {
|
|
19
|
+
const entry = this.cache.get(sessionId);
|
|
20
|
+
if (entry) {
|
|
21
|
+
// Update timestamp on access (LRU behavior)
|
|
22
|
+
entry.timestamp = Date.now();
|
|
23
|
+
// Move to end (most recently used)
|
|
24
|
+
this.cache.delete(sessionId);
|
|
25
|
+
this.cache.set(sessionId, entry);
|
|
26
|
+
return entry.identity;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
set(sessionId, identity) {
|
|
31
|
+
// Remove if already exists (to re-add at end)
|
|
32
|
+
this.cache.delete(sessionId);
|
|
33
|
+
// Evict oldest if at capacity
|
|
34
|
+
if (this.cache.size >= this.maxSize) {
|
|
35
|
+
const oldestKey = this.cache.keys().next().value;
|
|
36
|
+
if (oldestKey !== undefined) {
|
|
37
|
+
this.cache.delete(oldestKey);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
this.cache.set(sessionId, { identity, timestamp: Date.now() });
|
|
41
|
+
}
|
|
42
|
+
has(sessionId) {
|
|
43
|
+
return this.cache.has(sessionId);
|
|
44
|
+
}
|
|
45
|
+
size() {
|
|
46
|
+
return this.cache.size;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Global identity cache shared across all server instances
|
|
50
|
+
// This prevents duplicate identify events when server objects are recreated
|
|
51
|
+
const _globalIdentityCache = new IdentityCache(1000);
|
|
52
|
+
// Internal tracking storage
|
|
53
|
+
const _serverTracking = new WeakMap();
|
|
54
|
+
export function getServerTrackingData(server) {
|
|
55
|
+
return _serverTracking.get(server);
|
|
56
|
+
}
|
|
57
|
+
export function setServerTrackingData(server, data) {
|
|
58
|
+
_serverTracking.set(server, data);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Deep comparison of two UserIdentity objects
|
|
62
|
+
*/
|
|
63
|
+
export function areIdentitiesEqual(a, b) {
|
|
64
|
+
if (a.userId !== b.userId)
|
|
65
|
+
return false;
|
|
66
|
+
if (a.userName !== b.userName)
|
|
67
|
+
return false;
|
|
68
|
+
// Deep compare userData objects
|
|
69
|
+
const aData = a.userData || {};
|
|
70
|
+
const bData = b.userData || {};
|
|
71
|
+
const aKeys = Object.keys(aData);
|
|
72
|
+
const bKeys = Object.keys(bData);
|
|
73
|
+
if (aKeys.length !== bKeys.length)
|
|
74
|
+
return false;
|
|
75
|
+
for (const key of aKeys) {
|
|
76
|
+
if (!(key in bData))
|
|
77
|
+
return false;
|
|
78
|
+
if (JSON.stringify(aData[key]) !== JSON.stringify(bData[key]))
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Merges two UserIdentity objects, overwriting userId and userName,
|
|
85
|
+
* but merging userData fields
|
|
86
|
+
*/
|
|
87
|
+
export function mergeIdentities(previous, next) {
|
|
88
|
+
if (!previous) {
|
|
89
|
+
return next;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
userId: next.userId,
|
|
93
|
+
userName: next.userName,
|
|
94
|
+
userData: {
|
|
95
|
+
...(previous.userData || {}),
|
|
96
|
+
...(next.userData || {}),
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Handles user identification for a request.
|
|
102
|
+
* Calls the identify function if configured, compares with previous identity,
|
|
103
|
+
* and publishes an identify event only if the identity has changed.
|
|
104
|
+
*
|
|
105
|
+
* @param server - The MCP server instance
|
|
106
|
+
* @param data - The server tracking data
|
|
107
|
+
* @param request - The request object to pass to identify function
|
|
108
|
+
* @param extra - Optional extra parameters containing headers, sessionId, etc.
|
|
109
|
+
*/
|
|
110
|
+
export async function handleIdentify(server, data, request, extra) {
|
|
111
|
+
if (!data.options.identify) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const sessionId = data.sessionId;
|
|
115
|
+
let identifyEvent = {
|
|
116
|
+
sessionId: sessionId,
|
|
117
|
+
resourceName: request.params?.name || "Unknown",
|
|
118
|
+
eventType: WithinEventType.identify,
|
|
119
|
+
parameters: {
|
|
120
|
+
request: request,
|
|
121
|
+
extra: extra,
|
|
122
|
+
},
|
|
123
|
+
timestamp: new Date(),
|
|
124
|
+
redactionFn: data.options.redactSensitiveInformation,
|
|
125
|
+
};
|
|
126
|
+
try {
|
|
127
|
+
const rawIdentity = await data.options.identify(request, extra);
|
|
128
|
+
const identityResult = rawIdentity
|
|
129
|
+
? sanitizeIdentity(data.vendorSlug, rawIdentity, data.options.privacy)
|
|
130
|
+
: null;
|
|
131
|
+
if (identityResult) {
|
|
132
|
+
// Now use the (possibly updated) sessionId for all subsequent operations
|
|
133
|
+
const currentSessionId = data.sessionId;
|
|
134
|
+
// Check global cache first (works across server instance restarts)
|
|
135
|
+
const previousIdentity = _globalIdentityCache.get(currentSessionId);
|
|
136
|
+
// Merge identities (overwrite userId/userName, merge userData)
|
|
137
|
+
const mergedIdentity = mergeIdentities(previousIdentity, identityResult);
|
|
138
|
+
// Only publish if identity has changed
|
|
139
|
+
const hasChanged = !previousIdentity ||
|
|
140
|
+
!areIdentitiesEqual(previousIdentity, mergedIdentity);
|
|
141
|
+
// Update BOTH caches to keep them in sync
|
|
142
|
+
// Global cache: persists across server instances
|
|
143
|
+
_globalIdentityCache.set(currentSessionId, mergedIdentity);
|
|
144
|
+
// Per-server cache: used by getSessionInfo() for fast local access
|
|
145
|
+
data.identifiedSessions.set(data.sessionId, mergedIdentity);
|
|
146
|
+
data.sessionInfo.identifyActorGivenId = mergedIdentity.userId;
|
|
147
|
+
data.sessionInfo.identifyActorName = undefined;
|
|
148
|
+
data.sessionInfo.identifyActorData = mergedIdentity.userData || {};
|
|
149
|
+
setServerTrackingData(server, data);
|
|
150
|
+
if (hasChanged) {
|
|
151
|
+
writeToLog(`Identified session ${currentSessionId} (actor: ${mergedIdentity.userId})`);
|
|
152
|
+
publishEvent(server, identifyEvent);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
writeToLog(`Warning: Supplied identify function returned null for session ${sessionId}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
writeToLog(`Error: User supplied identify function threw an error while identifying session ${sessionId} - ${error}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Resolves the eventTags callback, validates the result, and returns validated tags.
|
|
165
|
+
* Returns null if no callback configured, callback returns nullish, or callback throws.
|
|
166
|
+
*/
|
|
167
|
+
export async function resolveEventTags(data, request, extra) {
|
|
168
|
+
if (!data.options.eventTags)
|
|
169
|
+
return null;
|
|
170
|
+
try {
|
|
171
|
+
const raw = (await data.options.eventTags(request, extra)) ?? null;
|
|
172
|
+
if (!raw)
|
|
173
|
+
return null;
|
|
174
|
+
return validateTags(raw);
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
writeToLog(`eventTags callback error: ${e}`);
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Resolves the eventProperties callback and returns the result.
|
|
183
|
+
* Returns null if no callback configured, callback returns nullish, or callback throws.
|
|
184
|
+
*/
|
|
185
|
+
export async function resolveEventProperties(data, request, extra) {
|
|
186
|
+
if (!data.options.eventProperties)
|
|
187
|
+
return null;
|
|
188
|
+
try {
|
|
189
|
+
return (await data.options.eventProperties(request, extra)) ?? null;
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
writeToLog(`eventProperties callback error: ${e}`);
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|