@sentry/junior 0.1.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.
@@ -0,0 +1,520 @@
1
+ // src/chat/observability.ts
2
+ import * as Sentry2 from "@sentry/nextjs";
3
+
4
+ // src/chat/logging.ts
5
+ import { AsyncLocalStorage } from "async_hooks";
6
+ import * as Sentry from "@sentry/nextjs";
7
+ var MAX_STRING_VALUE = 1200;
8
+ var SECRETS_RE = [
9
+ /\b(sk-[A-Za-z0-9_-]{20,})\b/g,
10
+ /\b(xox[baprs]-[A-Za-z0-9-]{10,})\b/g,
11
+ /\bBearer\s+([A-Za-z0-9._\-+=]{20,})\b/gi,
12
+ /\b[A-Z0-9_]+(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD)\s*[=:]\s*([^\s"']{8,})/gi,
13
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----/g
14
+ ];
15
+ var LEGACY_KEY_MAP = {
16
+ error: "error.message",
17
+ "error.stack": "exception.stacktrace",
18
+ "gen_ai.system": "gen_ai.provider.name",
19
+ "gen_ai.request.messages": "gen_ai.input.messages",
20
+ "gen_ai.response.text": "gen_ai.output.messages",
21
+ "messaging.conversation.id": "messaging.message.conversation_id",
22
+ bytes: "file.size",
23
+ media_type: "file.mime_type",
24
+ skillDir: "file.path",
25
+ root: "file.directory",
26
+ originalLength: "app.output.original_length",
27
+ parsedLength: "app.output.parsed_length",
28
+ directiveMode: "app.output.directive_mode",
29
+ fileCount: "app.output.file_count",
30
+ attempt: "app.retry.attempt",
31
+ steps: "app.ai.steps",
32
+ toolCalls: "app.ai.tool_calls",
33
+ toolResults: "app.ai.tool_results",
34
+ finishReason: "app.ai.finish_reason",
35
+ sources: "app.ai.sources",
36
+ generatedFiles: "app.ai.generated_files",
37
+ resultFiles: "app.ai.result_files",
38
+ responseMessages: "app.ai.response_messages",
39
+ stepDiagnostics: "app.ai.step_diagnostics",
40
+ inferredSkill: "app.skill.name",
41
+ inferredScore: "app.skill.score"
42
+ };
43
+ var contextStorage = new AsyncLocalStorage();
44
+ var logRecordSinks = /* @__PURE__ */ new Set();
45
+ var ANSI = {
46
+ reset: "\x1B[0m",
47
+ faint: "\x1B[2m",
48
+ red: "\x1B[31m",
49
+ yellow: "\x1B[33m",
50
+ green: "\x1B[32m",
51
+ blue: "\x1B[34m",
52
+ cyan: "\x1B[36m",
53
+ gray: "\x1B[90m"
54
+ };
55
+ var CONSOLE_PRIORITY_KEYS = [
56
+ "app.conversation.id",
57
+ "app.turn.id",
58
+ "app.agent.id",
59
+ "event.name",
60
+ "error.message",
61
+ "messaging.message.id",
62
+ "messaging.message.conversation_id",
63
+ "messaging.destination.name",
64
+ "enduser.id",
65
+ "app.run.id",
66
+ "app.message.kind",
67
+ "app.trace_id",
68
+ "app.span_id"
69
+ ];
70
+ var CONSOLE_PRIORITY_INDEX = new Map(
71
+ CONSOLE_PRIORITY_KEYS.map((key, index) => [key, index])
72
+ );
73
+ function getSentryEnvironment() {
74
+ return (process.env.SENTRY_ENVIRONMENT ?? process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT ?? process.env.VERCEL_ENV ?? process.env.NODE_ENV ?? "").trim().toLowerCase();
75
+ }
76
+ function shouldSuppressInfoLog(_level) {
77
+ return false;
78
+ }
79
+ function shouldEmitConsole(level) {
80
+ if (process.env.NODE_ENV === "test") {
81
+ return level === "error";
82
+ }
83
+ return getSentryEnvironment() !== "production";
84
+ }
85
+ function redactSecrets(input) {
86
+ let out = input;
87
+ for (const pattern of SECRETS_RE) {
88
+ out = out.replace(pattern, (full, token) => {
89
+ if (full.includes("PRIVATE KEY")) {
90
+ const lines = full.trim().split("\n");
91
+ return lines.length >= 2 ? `${lines[0]}
92
+ ...redacted...
93
+ ${lines[lines.length - 1]}` : "***PRIVATE KEY***";
94
+ }
95
+ if (typeof token !== "string") {
96
+ return "***";
97
+ }
98
+ if (token.length < 12) {
99
+ return full.replace(token, "***");
100
+ }
101
+ return full.replace(token, `${token.slice(0, 4)}...${token.slice(-4)}`);
102
+ });
103
+ }
104
+ return out;
105
+ }
106
+ function toSnakeCase(value) {
107
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9_]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
108
+ }
109
+ function isSemanticKey(key) {
110
+ return /^[a-z][a-z0-9_]*(\.[a-z0-9_][a-z0-9_-]*)+$/.test(key);
111
+ }
112
+ function normalizeAttributeKey(key) {
113
+ const mapped = LEGACY_KEY_MAP[key];
114
+ if (mapped) {
115
+ return mapped;
116
+ }
117
+ if (isSemanticKey(key)) {
118
+ return key;
119
+ }
120
+ if (key === "platform") return "app.platform";
121
+ if (key === "request.id") return "app.request.id";
122
+ const snake = toSnakeCase(key);
123
+ if (!snake) {
124
+ return "app.attribute";
125
+ }
126
+ return `app.${snake}`;
127
+ }
128
+ function sanitizePrimitive(value) {
129
+ if (value === null || value === void 0) return void 0;
130
+ if (typeof value === "string") {
131
+ const trimmed = value.trim();
132
+ if (!trimmed) return void 0;
133
+ const redacted = redactSecrets(trimmed);
134
+ return redacted.length > MAX_STRING_VALUE ? `${redacted.slice(0, MAX_STRING_VALUE)}...` : redacted;
135
+ }
136
+ if (typeof value === "number") {
137
+ return Number.isFinite(value) ? value : void 0;
138
+ }
139
+ if (typeof value === "boolean") return value;
140
+ if (value instanceof Error) {
141
+ return redactSecrets(value.message);
142
+ }
143
+ try {
144
+ const json = JSON.stringify(value);
145
+ if (!json) return void 0;
146
+ const redacted = redactSecrets(json);
147
+ return redacted.length > MAX_STRING_VALUE ? `${redacted.slice(0, MAX_STRING_VALUE)}...` : redacted;
148
+ } catch {
149
+ return void 0;
150
+ }
151
+ }
152
+ function sanitizeValue(value) {
153
+ if (Array.isArray(value)) {
154
+ const sanitized = value.filter((entry) => typeof entry === "string").map((entry) => sanitizePrimitive(entry)).filter((entry) => typeof entry === "string");
155
+ return sanitized.length > 0 ? sanitized : void 0;
156
+ }
157
+ return sanitizePrimitive(value);
158
+ }
159
+ function contextToAttributes(context) {
160
+ const attributes = {
161
+ "app.conversation.id": context.conversationId,
162
+ "app.turn.id": context.turnId,
163
+ "app.agent.id": context.agentId,
164
+ "app.platform": context.platform,
165
+ "app.request.id": context.requestId,
166
+ "messaging.system": context.platform === "slack" ? "slack" : context.platform,
167
+ "messaging.message.conversation_id": context.slackThreadId,
168
+ "messaging.destination.name": context.slackChannelId,
169
+ "enduser.id": context.slackUserId,
170
+ "enduser.pseudo_id": context.slackUserName,
171
+ "app.run.id": context.runId,
172
+ "app.assistant.username": context.assistantUserName,
173
+ "gen_ai.request.model": context.modelId,
174
+ "app.skill.name": context.skillName,
175
+ "http.request.method": context.httpMethod,
176
+ "url.path": context.httpPath,
177
+ "url.full": context.urlFull,
178
+ "user_agent.original": context.userAgent
179
+ };
180
+ const normalized = {};
181
+ for (const [key, value] of Object.entries(attributes)) {
182
+ const sanitized = sanitizeValue(value);
183
+ if (sanitized !== void 0) normalized[key] = sanitized;
184
+ }
185
+ return normalized;
186
+ }
187
+ function getTraceCorrelationAttributes() {
188
+ const sentry = Sentry;
189
+ if (typeof sentry.getActiveSpan !== "function" || typeof sentry.spanToJSON !== "function") {
190
+ return {};
191
+ }
192
+ try {
193
+ const span = sentry.getActiveSpan();
194
+ if (!span) return {};
195
+ const json = sentry.spanToJSON(span);
196
+ const attrs = {};
197
+ if (json.trace_id) attrs.trace_id = json.trace_id;
198
+ if (json.span_id) attrs.span_id = json.span_id;
199
+ return attrs;
200
+ } catch {
201
+ return {};
202
+ }
203
+ }
204
+ function mergeAttributes(...maps) {
205
+ const merged = {};
206
+ for (const map of maps) {
207
+ if (!map) continue;
208
+ for (const [rawKey, rawValue] of Object.entries(map)) {
209
+ const key = normalizeAttributeKey(rawKey);
210
+ const value = sanitizeValue(rawValue);
211
+ if (value !== void 0) {
212
+ merged[key] = value;
213
+ }
214
+ }
215
+ }
216
+ return merged;
217
+ }
218
+ function emitSentry(level, body, attributes) {
219
+ if (shouldSuppressInfoLog(level)) {
220
+ return;
221
+ }
222
+ const sentry = Sentry;
223
+ const loggerFn = sentry.logger?.[level];
224
+ if (typeof loggerFn === "function") {
225
+ loggerFn(body, attributes);
226
+ return;
227
+ }
228
+ Sentry.withScope((scope) => {
229
+ for (const [key, value] of Object.entries(attributes)) {
230
+ scope.setExtra(key, value);
231
+ }
232
+ const sentryLevel = level === "warn" ? "warning" : level;
233
+ Sentry.captureMessage(body, sentryLevel);
234
+ });
235
+ }
236
+ function formatConsoleLevel(level) {
237
+ if (level === "debug") return "DBG";
238
+ if (level === "info") return "INF";
239
+ if (level === "warn") return "WRN";
240
+ return "ERR";
241
+ }
242
+ function consoleLevelColor(level) {
243
+ if (level === "error") return ANSI.red;
244
+ if (level === "warn") return ANSI.yellow;
245
+ if (level === "info") return ANSI.green;
246
+ return ANSI.blue;
247
+ }
248
+ function quoteConsoleValue(value) {
249
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
250
+ }
251
+ function formatConsoleValue(value) {
252
+ if (typeof value === "number" || typeof value === "boolean") {
253
+ return String(value);
254
+ }
255
+ if (Array.isArray(value)) {
256
+ return quoteConsoleValue(JSON.stringify(value));
257
+ }
258
+ if (/^[A-Za-z0-9._:/@+-]+$/.test(value)) {
259
+ return value;
260
+ }
261
+ return quoteConsoleValue(value);
262
+ }
263
+ function formatConsoleLine(level, body, attributes) {
264
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
265
+ const useColor = process.env.NODE_ENV === "development" && Boolean(process.stdout?.isTTY);
266
+ const levelColor = consoleLevelColor(level);
267
+ const colorize = (text, color) => useColor ? `${color}${text}${ANSI.reset}` : text;
268
+ const parts = [
269
+ `${colorize(timestamp, ANSI.gray)} ${colorize(formatConsoleLevel(level), levelColor)} ${body}`
270
+ ];
271
+ const sortedAttributes = Object.entries(attributes).sort(([left], [right]) => {
272
+ const leftRank = CONSOLE_PRIORITY_INDEX.get(left);
273
+ const rightRank = CONSOLE_PRIORITY_INDEX.get(right);
274
+ if (leftRank !== void 0 || rightRank !== void 0) {
275
+ if (leftRank === void 0) return 1;
276
+ if (rightRank === void 0) return -1;
277
+ return leftRank - rightRank;
278
+ }
279
+ return left.localeCompare(right);
280
+ });
281
+ for (const [key, value] of sortedAttributes) {
282
+ const rendered = `${colorize(key, ANSI.cyan)}=${colorize(formatConsoleValue(value), ANSI.faint)}`;
283
+ parts.push(rendered);
284
+ }
285
+ return parts.join(" ");
286
+ }
287
+ function emitConsole(level, _eventName, body, attributes) {
288
+ if (!shouldEmitConsole(level)) {
289
+ return;
290
+ }
291
+ const line = formatConsoleLine(level, body, attributes);
292
+ if (level === "error") {
293
+ console.error(line);
294
+ return;
295
+ }
296
+ if (level === "warn") {
297
+ console.warn(line);
298
+ return;
299
+ }
300
+ if (level === "info") {
301
+ console.info(line);
302
+ return;
303
+ }
304
+ console.debug(line);
305
+ }
306
+ function emit(level, eventName, attrs = {}, body) {
307
+ const contextAttributes = contextStorage.getStore() ?? {};
308
+ const traceAttributes = getTraceCorrelationAttributes();
309
+ const normalizedEventName = toSnakeCase(eventName);
310
+ const message = body ? redactSecrets(body) : normalizedEventName;
311
+ const attributes = mergeAttributes(
312
+ contextAttributes,
313
+ traceAttributes,
314
+ {
315
+ "event.name": normalizedEventName,
316
+ ...attrs
317
+ }
318
+ );
319
+ for (const sink of logRecordSinks) {
320
+ try {
321
+ sink({
322
+ level,
323
+ eventName: normalizedEventName,
324
+ body: message,
325
+ attributes
326
+ });
327
+ } catch {
328
+ }
329
+ }
330
+ emitConsole(level, normalizedEventName, message, attributes);
331
+ emitSentry(level, message, attributes);
332
+ }
333
+ var log = {
334
+ debug(eventName, attrs = {}, body) {
335
+ emit("debug", eventName, attrs, body);
336
+ },
337
+ info(eventName, attrs = {}, body) {
338
+ emit("info", eventName, attrs, body);
339
+ },
340
+ warn(eventName, attrs = {}, body) {
341
+ emit("warn", eventName, attrs, body);
342
+ },
343
+ error(eventName, attrs = {}, body) {
344
+ emit("error", eventName, attrs, body);
345
+ },
346
+ exception(eventName, error, attrs = {}, body) {
347
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
348
+ emit("error", eventName, {
349
+ ...attrs,
350
+ "error.type": normalizedError.name,
351
+ "error.message": normalizedError.message,
352
+ "exception.type": normalizedError.name,
353
+ "exception.message": normalizedError.message,
354
+ "exception.stacktrace": normalizedError.stack
355
+ }, body ?? normalizedError.message);
356
+ Sentry.withScope((scope) => {
357
+ for (const [key, value] of Object.entries(mergeAttributes(contextStorage.getStore(), attrs))) {
358
+ scope.setExtra(key, value);
359
+ }
360
+ Sentry.captureException(normalizedError);
361
+ });
362
+ }
363
+ };
364
+ function withLogContext(context, callback) {
365
+ const next = mergeAttributes(contextStorage.getStore(), contextToAttributes(context));
366
+ return contextStorage.run(next, callback);
367
+ }
368
+ function setLogContext(context) {
369
+ const merged = mergeAttributes(contextStorage.getStore(), contextToAttributes(context));
370
+ contextStorage.enterWith(merged);
371
+ }
372
+ function createLogContextFromRequest(request, context = {}) {
373
+ const url = new URL(request.url);
374
+ return {
375
+ ...context,
376
+ requestId: context.requestId ?? request.headers.get("x-request-id") ?? void 0,
377
+ httpMethod: request.method,
378
+ httpPath: url.pathname,
379
+ urlFull: url.toString(),
380
+ userAgent: request.headers.get("user-agent") ?? void 0
381
+ };
382
+ }
383
+ function toSpanAttributes(context) {
384
+ const attrs = contextToAttributes(context);
385
+ return Object.fromEntries(
386
+ Object.entries(attrs).filter(([, value]) => typeof value === "string" && value.length > 0)
387
+ );
388
+ }
389
+ function setSentryTagsFromContext(context) {
390
+ const attrs = contextToAttributes(context);
391
+ for (const [key, value] of Object.entries(attrs)) {
392
+ if (typeof value === "string" && value.length > 0) {
393
+ Sentry.setTag(key, value);
394
+ }
395
+ }
396
+ if (context.slackUserId) {
397
+ Sentry.setUser({ id: context.slackUserId, username: context.slackUserName });
398
+ }
399
+ }
400
+
401
+ // src/chat/observability.ts
402
+ function toSpanAttributeValue(value) {
403
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
404
+ return value;
405
+ }
406
+ if (!Array.isArray(value)) {
407
+ return void 0;
408
+ }
409
+ const sanitized = value.filter((entry) => typeof entry === "string");
410
+ return sanitized.length > 0 ? sanitized : void 0;
411
+ }
412
+ function toContextAndAttributes(context, attributes) {
413
+ return {
414
+ ...toSpanAttributes(context),
415
+ ...attributes
416
+ };
417
+ }
418
+ function logWithLevel(level, eventName, attributes = {}, body) {
419
+ if (level === "info") {
420
+ log.info(eventName, attributes, body);
421
+ return;
422
+ }
423
+ if (level === "warn") {
424
+ log.warn(eventName, attributes, body);
425
+ return;
426
+ }
427
+ log.error(eventName, attributes, body);
428
+ }
429
+ function logInfo(eventName, context = {}, attributes = {}, body) {
430
+ logWithLevel("info", eventName, toContextAndAttributes(context, attributes), body);
431
+ }
432
+ function logWarn(eventName, context = {}, attributes = {}, body) {
433
+ logWithLevel("warn", eventName, toContextAndAttributes(context, attributes), body);
434
+ }
435
+ function logError(eventName, context = {}, attributes = {}, body) {
436
+ logWithLevel("error", eventName, toContextAndAttributes(context, attributes), body);
437
+ }
438
+ function logException(error, eventName, context = {}, attributes = {}, body) {
439
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
440
+ log.exception(eventName, normalizedError, toContextAndAttributes(context, attributes), body);
441
+ }
442
+ function setTags(context = {}) {
443
+ setLogContext(context);
444
+ setSentryTagsFromContext(context);
445
+ }
446
+ function createRequestContext(request, context = {}) {
447
+ return createLogContextFromRequest(request, context);
448
+ }
449
+ async function withContext(context, callback) {
450
+ return withLogContext(context, callback);
451
+ }
452
+ async function withSpan(name, op, context, callback, attributes = {}) {
453
+ const normalizedAttributes = {};
454
+ for (const [key, value] of Object.entries(attributes)) {
455
+ const normalizedValue = toSpanAttributeValue(value);
456
+ if (normalizedValue !== void 0) {
457
+ normalizedAttributes[key] = normalizedValue;
458
+ }
459
+ }
460
+ return withLogContext(
461
+ context,
462
+ () => Sentry2.startSpan(
463
+ {
464
+ name,
465
+ op,
466
+ attributes: {
467
+ ...toSpanAttributes(context),
468
+ ...normalizedAttributes
469
+ }
470
+ },
471
+ callback
472
+ )
473
+ );
474
+ }
475
+ function setSpanAttributes(attributes) {
476
+ const sentry = Sentry2;
477
+ const span = sentry.getActiveSpan?.();
478
+ if (!span) {
479
+ return;
480
+ }
481
+ const setAttribute = span.setAttribute;
482
+ if (typeof setAttribute !== "function") {
483
+ return;
484
+ }
485
+ for (const [key, value] of Object.entries(attributes)) {
486
+ const normalizedValue = toSpanAttributeValue(value);
487
+ if (normalizedValue !== void 0) {
488
+ setAttribute.call(span, key, normalizedValue);
489
+ }
490
+ }
491
+ }
492
+ function setSpanStatus(status) {
493
+ const sentry = Sentry2;
494
+ const span = sentry.getActiveSpan?.();
495
+ if (!span) {
496
+ return;
497
+ }
498
+ const setStatus = span.setStatus;
499
+ if (typeof setStatus !== "function") {
500
+ return;
501
+ }
502
+ setStatus.call(span, status === "ok" ? "ok" : "internal_error");
503
+ }
504
+ function toOptionalString(value) {
505
+ return typeof value === "string" && value.trim() ? value : void 0;
506
+ }
507
+
508
+ export {
509
+ logInfo,
510
+ logWarn,
511
+ logError,
512
+ logException,
513
+ setTags,
514
+ createRequestContext,
515
+ withContext,
516
+ withSpan,
517
+ setSpanAttributes,
518
+ setSpanStatus,
519
+ toOptionalString
520
+ };