@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,333 @@
1
+ import {
2
+ logWarn
3
+ } from "./chunk-BBOVH5RF.js";
4
+
5
+ // src/chat/config.ts
6
+ var MIN_AGENT_TURN_TIMEOUT_MS = 10 * 1e3;
7
+ var DEFAULT_AGENT_TURN_TIMEOUT_MS = 12 * 60 * 1e3;
8
+ var DEFAULT_QUEUE_CALLBACK_MAX_DURATION_SECONDS = 800;
9
+ var TURN_TIMEOUT_BUFFER_SECONDS = 20;
10
+ function parseAgentTurnTimeoutMs(rawValue, maxTimeoutMs) {
11
+ const value = Number.parseInt(rawValue ?? "", 10);
12
+ if (Number.isNaN(value)) {
13
+ return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, Math.min(DEFAULT_AGENT_TURN_TIMEOUT_MS, maxTimeoutMs));
14
+ }
15
+ return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, Math.min(value, maxTimeoutMs));
16
+ }
17
+ function resolveQueueCallbackMaxDurationSeconds() {
18
+ const value = Number.parseInt(process.env.QUEUE_CALLBACK_MAX_DURATION_SECONDS ?? "", 10);
19
+ if (Number.isNaN(value) || value <= 0) {
20
+ return DEFAULT_QUEUE_CALLBACK_MAX_DURATION_SECONDS;
21
+ }
22
+ return value;
23
+ }
24
+ function resolveMaxTurnTimeoutMs(queueCallbackMaxDurationSeconds) {
25
+ const budgetSeconds = queueCallbackMaxDurationSeconds - TURN_TIMEOUT_BUFFER_SECONDS;
26
+ return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, budgetSeconds * 1e3);
27
+ }
28
+ function buildBotConfig() {
29
+ const queueCallbackMaxDurationSeconds = resolveQueueCallbackMaxDurationSeconds();
30
+ const maxTurnTimeoutMs = resolveMaxTurnTimeoutMs(queueCallbackMaxDurationSeconds);
31
+ return {
32
+ userName: process.env.JUNIOR_BOT_NAME ?? "junior",
33
+ modelId: process.env.AI_MODEL ?? "anthropic/claude-sonnet-4.6",
34
+ fastModelId: process.env.AI_FAST_MODEL ?? process.env.AI_MODEL ?? "anthropic/claude-haiku-4.5",
35
+ turnTimeoutMs: parseAgentTurnTimeoutMs(process.env.AGENT_TURN_TIMEOUT_MS, maxTurnTimeoutMs)
36
+ };
37
+ }
38
+ var botConfig = buildBotConfig();
39
+ function toOptionalTrimmed(value) {
40
+ if (!value) {
41
+ return void 0;
42
+ }
43
+ const trimmed = value.trim();
44
+ return trimmed.length > 0 ? trimmed : void 0;
45
+ }
46
+ function getSlackBotToken() {
47
+ return toOptionalTrimmed(process.env.SLACK_BOT_TOKEN) ?? toOptionalTrimmed(process.env.SLACK_BOT_USER_TOKEN);
48
+ }
49
+ function getSlackSigningSecret() {
50
+ return toOptionalTrimmed(process.env.SLACK_SIGNING_SECRET);
51
+ }
52
+ function getSlackClientId() {
53
+ return toOptionalTrimmed(process.env.SLACK_CLIENT_ID);
54
+ }
55
+ function getSlackClientSecret() {
56
+ return toOptionalTrimmed(process.env.SLACK_CLIENT_SECRET);
57
+ }
58
+ function hasRedisConfig() {
59
+ return Boolean(process.env.REDIS_URL);
60
+ }
61
+
62
+ // src/chat/slack-actions/client.ts
63
+ import { WebClient } from "@slack/web-api";
64
+ var SlackActionError = class extends Error {
65
+ code;
66
+ apiError;
67
+ needed;
68
+ provided;
69
+ statusCode;
70
+ requestId;
71
+ errorData;
72
+ retryAfterSeconds;
73
+ detail;
74
+ detailLine;
75
+ detailRule;
76
+ constructor(message, code, options = {}) {
77
+ super(message);
78
+ this.name = "SlackActionError";
79
+ this.code = code;
80
+ this.apiError = options.apiError;
81
+ this.needed = options.needed;
82
+ this.provided = options.provided;
83
+ this.statusCode = options.statusCode;
84
+ this.requestId = options.requestId;
85
+ this.errorData = options.errorData;
86
+ this.retryAfterSeconds = options.retryAfterSeconds;
87
+ this.detail = options.detail;
88
+ this.detailLine = options.detailLine;
89
+ this.detailRule = options.detailRule;
90
+ }
91
+ };
92
+ function serializeSlackErrorData(data) {
93
+ if (!data || typeof data !== "object") {
94
+ return void 0;
95
+ }
96
+ const filtered = Object.fromEntries(
97
+ Object.entries(data).filter(([key]) => key !== "error")
98
+ );
99
+ if (Object.keys(filtered).length === 0) {
100
+ return void 0;
101
+ }
102
+ try {
103
+ const serialized = JSON.stringify(filtered);
104
+ return serialized.length <= 600 ? serialized : `${serialized.slice(0, 597)}...`;
105
+ } catch {
106
+ return void 0;
107
+ }
108
+ }
109
+ function getHeaderString(headers, name) {
110
+ if (!headers || typeof headers !== "object") {
111
+ return void 0;
112
+ }
113
+ const key = name.toLowerCase();
114
+ const entries = headers;
115
+ for (const [entryKey, value] of Object.entries(entries)) {
116
+ if (entryKey.toLowerCase() !== key) continue;
117
+ if (typeof value === "string") return value;
118
+ if (Array.isArray(value)) {
119
+ const first = value.find((entry) => typeof entry === "string");
120
+ return typeof first === "string" ? first : void 0;
121
+ }
122
+ }
123
+ return void 0;
124
+ }
125
+ function parseSlackCanvasDetail(detail) {
126
+ if (typeof detail !== "string") {
127
+ return {};
128
+ }
129
+ const trimmed = detail.trim();
130
+ if (!trimmed) {
131
+ return {};
132
+ }
133
+ const parsed = {
134
+ detail: trimmed
135
+ };
136
+ const lineMatch = trimmed.match(/line\s+(\d+):/i);
137
+ if (lineMatch) {
138
+ const line = Number.parseInt(lineMatch[1] ?? "", 10);
139
+ if (Number.isFinite(line)) {
140
+ parsed.detailLine = line;
141
+ }
142
+ }
143
+ if (/unsupported heading depth/i.test(trimmed)) {
144
+ parsed.detailRule = "unsupported_heading_depth";
145
+ }
146
+ return parsed;
147
+ }
148
+ var client = null;
149
+ function normalizeSlackConversationId(channelId) {
150
+ if (!channelId) return void 0;
151
+ const trimmed = channelId.trim();
152
+ if (!trimmed) return void 0;
153
+ if (!trimmed.startsWith("slack:")) {
154
+ return trimmed;
155
+ }
156
+ const parts = trimmed.split(":");
157
+ return parts[1]?.trim() || void 0;
158
+ }
159
+ function getClient() {
160
+ if (client) return client;
161
+ const token = getSlackBotToken();
162
+ if (!token) {
163
+ throw new SlackActionError(
164
+ "SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack canvas/list actions in this service",
165
+ "missing_token"
166
+ );
167
+ }
168
+ client = new WebClient(token);
169
+ return client;
170
+ }
171
+ function mapSlackError(error) {
172
+ if (error instanceof SlackActionError) {
173
+ return error;
174
+ }
175
+ const candidate = error;
176
+ const apiError = candidate.data?.error;
177
+ const message = candidate.message ?? "Slack action failed";
178
+ const baseOptions = {
179
+ apiError,
180
+ statusCode: candidate.statusCode,
181
+ requestId: getHeaderString(candidate.headers, "x-slack-req-id"),
182
+ errorData: serializeSlackErrorData(candidate.data),
183
+ ...parseSlackCanvasDetail(candidate.data?.detail)
184
+ };
185
+ if (apiError === "missing_scope") {
186
+ return new SlackActionError(message, "missing_scope", {
187
+ ...baseOptions,
188
+ needed: candidate.data?.needed,
189
+ provided: candidate.data?.provided
190
+ });
191
+ }
192
+ if (apiError === "not_in_channel") {
193
+ return new SlackActionError(message, "not_in_channel", baseOptions);
194
+ }
195
+ if (apiError === "invalid_arguments") {
196
+ return new SlackActionError(message, "invalid_arguments", baseOptions);
197
+ }
198
+ if (apiError === "invalid_name") {
199
+ return new SlackActionError(message, "invalid_arguments", baseOptions);
200
+ }
201
+ if (apiError === "not_found") {
202
+ return new SlackActionError(message, "not_found", baseOptions);
203
+ }
204
+ if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") {
205
+ return new SlackActionError(message, "feature_unavailable", baseOptions);
206
+ }
207
+ if (apiError === "canvas_creation_failed") {
208
+ return new SlackActionError(message, "canvas_creation_failed", baseOptions);
209
+ }
210
+ if (apiError === "canvas_editing_failed") {
211
+ return new SlackActionError(message, "canvas_editing_failed", baseOptions);
212
+ }
213
+ if (candidate.code === "slack_webapi_rate_limited_error" || candidate.statusCode === 429) {
214
+ return new SlackActionError(message, "rate_limited", {
215
+ ...baseOptions,
216
+ retryAfterSeconds: candidate.retryAfter
217
+ });
218
+ }
219
+ return new SlackActionError(message, "internal_error", baseOptions);
220
+ }
221
+ function sleep(ms) {
222
+ return new Promise((resolve) => setTimeout(resolve, ms));
223
+ }
224
+ async function withSlackRetries(task, maxAttempts = 3, context = {}) {
225
+ let attempt = 0;
226
+ while (attempt < maxAttempts) {
227
+ attempt += 1;
228
+ try {
229
+ return await task();
230
+ } catch (error) {
231
+ const mapped = mapSlackError(error);
232
+ const isRetryable = mapped.code === "rate_limited";
233
+ const baseLogAttributes = {
234
+ "app.slack.action": context.action ?? "unknown",
235
+ "app.slack.error_code": mapped.code,
236
+ ...mapped.apiError ? { "app.slack.api_error": mapped.apiError } : {},
237
+ ...mapped.detail ? { "app.slack.detail": mapped.detail } : {},
238
+ ...mapped.detailLine !== void 0 ? { "app.slack.detail_line": mapped.detailLine } : {},
239
+ ...mapped.detailRule ? { "app.slack.detail_rule": mapped.detailRule } : {},
240
+ ...mapped.requestId ? { "app.slack.request_id": mapped.requestId } : {},
241
+ ...mapped.statusCode !== void 0 ? { "http.response.status_code": mapped.statusCode } : {},
242
+ ...context.attributes ?? {}
243
+ };
244
+ if (!isRetryable || attempt >= maxAttempts) {
245
+ logWarn(
246
+ "slack_action_failed",
247
+ {},
248
+ {
249
+ ...baseLogAttributes,
250
+ ...mapped.errorData ? { "app.slack.error_data": mapped.errorData } : {}
251
+ },
252
+ "Slack action failed"
253
+ );
254
+ throw mapped;
255
+ }
256
+ logWarn(
257
+ "slack_action_retrying",
258
+ {},
259
+ {
260
+ ...baseLogAttributes,
261
+ "app.slack.retry_attempt": attempt
262
+ },
263
+ "Retrying Slack action after transient failure"
264
+ );
265
+ const retryAfterMs = mapped.code === "rate_limited" && mapped.retryAfterSeconds && mapped.retryAfterSeconds > 0 ? mapped.retryAfterSeconds * 1e3 : void 0;
266
+ const backoffMs = Math.min(2e3, 250 * 2 ** (attempt - 1));
267
+ await sleep(retryAfterMs ?? backoffMs);
268
+ }
269
+ }
270
+ throw new SlackActionError("Slack action exhausted retries", "internal_error");
271
+ }
272
+ function getSlackClient() {
273
+ return getClient();
274
+ }
275
+ function isDmChannel(channelId) {
276
+ const normalized = normalizeSlackConversationId(channelId);
277
+ return Boolean(normalized && normalized.startsWith("D"));
278
+ }
279
+ function isConversationScopedChannel(channelId) {
280
+ const normalized = normalizeSlackConversationId(channelId);
281
+ if (!normalized) return false;
282
+ return normalized.startsWith("C") || normalized.startsWith("G") || normalized.startsWith("D");
283
+ }
284
+ function isConversationChannel(channelId) {
285
+ const normalized = normalizeSlackConversationId(channelId);
286
+ if (!normalized) return false;
287
+ return normalized.startsWith("C") || normalized.startsWith("G");
288
+ }
289
+ async function getFilePermalink(fileId) {
290
+ const client2 = getClient();
291
+ const response = await withSlackRetries(
292
+ () => client2.files.info({
293
+ file: fileId
294
+ })
295
+ );
296
+ return response.file?.permalink;
297
+ }
298
+ async function downloadPrivateSlackFile(url) {
299
+ const token = getSlackBotToken();
300
+ if (!token) {
301
+ throw new SlackActionError(
302
+ "SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack file downloads in this service",
303
+ "missing_token"
304
+ );
305
+ }
306
+ const response = await fetch(url, {
307
+ headers: {
308
+ Authorization: `Bearer ${token}`
309
+ }
310
+ });
311
+ if (!response.ok) {
312
+ throw new Error(`Slack file download failed: ${response.status}`);
313
+ }
314
+ return Buffer.from(await response.arrayBuffer());
315
+ }
316
+
317
+ export {
318
+ botConfig,
319
+ getSlackBotToken,
320
+ getSlackSigningSecret,
321
+ getSlackClientId,
322
+ getSlackClientSecret,
323
+ hasRedisConfig,
324
+ SlackActionError,
325
+ normalizeSlackConversationId,
326
+ withSlackRetries,
327
+ getSlackClient,
328
+ isDmChannel,
329
+ isConversationScopedChannel,
330
+ isConversationChannel,
331
+ getFilePermalink,
332
+ downloadPrivateSlackFile
333
+ };
@@ -0,0 +1,203 @@
1
+ import {
2
+ getSlackClient,
3
+ normalizeSlackConversationId,
4
+ withSlackRetries
5
+ } from "./chunk-GDNDYMGX.js";
6
+
7
+ // src/chat/slack-actions/channel.ts
8
+ async function postMessageToChannel(input) {
9
+ const client = getSlackClient();
10
+ const channelId = normalizeSlackConversationId(input.channelId);
11
+ if (!channelId) {
12
+ throw new Error("Slack channel message posting requires a valid channel ID");
13
+ }
14
+ const response = await withSlackRetries(
15
+ () => client.chat.postMessage({
16
+ channel: channelId,
17
+ text: input.text,
18
+ mrkdwn: true
19
+ }),
20
+ 3,
21
+ { action: "chat.postMessage" }
22
+ );
23
+ if (!response.ts) {
24
+ throw new Error("Slack channel message posted without ts");
25
+ }
26
+ let permalink;
27
+ try {
28
+ const permalinkResponse = await withSlackRetries(
29
+ () => client.chat.getPermalink({
30
+ channel: channelId,
31
+ message_ts: response.ts
32
+ }),
33
+ 3,
34
+ { action: "chat.getPermalink" }
35
+ );
36
+ permalink = permalinkResponse.permalink;
37
+ } catch {
38
+ }
39
+ return {
40
+ ts: response.ts,
41
+ permalink
42
+ };
43
+ }
44
+ async function addReactionToMessage(input) {
45
+ const client = getSlackClient();
46
+ const channelId = normalizeSlackConversationId(input.channelId);
47
+ if (!channelId) {
48
+ throw new Error("Slack reaction requires a valid channel ID");
49
+ }
50
+ const timestamp = input.timestamp.trim();
51
+ if (!timestamp) {
52
+ throw new Error("Slack reaction requires a target message timestamp");
53
+ }
54
+ const emoji = input.emoji.trim().replaceAll(":", "");
55
+ if (!emoji) {
56
+ throw new Error("Slack reaction requires a non-empty emoji name");
57
+ }
58
+ await withSlackRetries(
59
+ () => client.reactions.add({
60
+ channel: channelId,
61
+ timestamp,
62
+ name: emoji
63
+ }),
64
+ 3,
65
+ { action: "reactions.add" }
66
+ );
67
+ return { ok: true };
68
+ }
69
+ async function removeReactionFromMessage(input) {
70
+ const client = getSlackClient();
71
+ const channelId = normalizeSlackConversationId(input.channelId);
72
+ if (!channelId) {
73
+ throw new Error("Slack reaction requires a valid channel ID");
74
+ }
75
+ const timestamp = input.timestamp.trim();
76
+ if (!timestamp) {
77
+ throw new Error("Slack reaction requires a target message timestamp");
78
+ }
79
+ const emoji = input.emoji.trim().replaceAll(":", "");
80
+ if (!emoji) {
81
+ throw new Error("Slack reaction requires a non-empty emoji name");
82
+ }
83
+ await withSlackRetries(
84
+ () => client.reactions.remove({
85
+ channel: channelId,
86
+ timestamp,
87
+ name: emoji
88
+ }),
89
+ 3,
90
+ { action: "reactions.remove" }
91
+ );
92
+ return { ok: true };
93
+ }
94
+ async function listChannelMessages(input) {
95
+ const client = getSlackClient();
96
+ const channelId = normalizeSlackConversationId(input.channelId);
97
+ if (!channelId) {
98
+ throw new Error("Slack channel history lookup requires a valid channel ID");
99
+ }
100
+ const targetLimit = Math.max(1, Math.min(input.limit, 1e3));
101
+ const maxPages = Math.max(1, Math.min(input.maxPages ?? 5, 10));
102
+ const messages = [];
103
+ let cursor = input.cursor;
104
+ let pages = 0;
105
+ while (messages.length < targetLimit && pages < maxPages) {
106
+ pages += 1;
107
+ const pageLimit = Math.max(1, Math.min(200, targetLimit - messages.length));
108
+ const response = await withSlackRetries(
109
+ () => client.conversations.history({
110
+ channel: channelId,
111
+ limit: pageLimit,
112
+ cursor,
113
+ oldest: input.oldest,
114
+ latest: input.latest,
115
+ inclusive: input.inclusive
116
+ }),
117
+ 3,
118
+ { action: "conversations.history" }
119
+ );
120
+ const batch = response.messages ?? [];
121
+ messages.push(...batch);
122
+ cursor = response.response_metadata?.next_cursor || void 0;
123
+ if (!cursor) {
124
+ break;
125
+ }
126
+ }
127
+ return {
128
+ messages: messages.slice(0, targetLimit),
129
+ nextCursor: cursor
130
+ };
131
+ }
132
+ async function listChannelMembers(input) {
133
+ const client = getSlackClient();
134
+ const channelId = normalizeSlackConversationId(input.channelId);
135
+ if (!channelId) {
136
+ throw new Error("Slack channel member lookup requires a valid channel ID");
137
+ }
138
+ const targetLimit = Math.max(1, Math.min(input.limit, 200));
139
+ const response = await withSlackRetries(
140
+ () => client.conversations.members({
141
+ channel: channelId,
142
+ limit: targetLimit,
143
+ cursor: input.cursor
144
+ }),
145
+ 3,
146
+ { action: "conversations.members" }
147
+ );
148
+ const members = (response.members ?? []).slice(0, targetLimit);
149
+ return {
150
+ members: members.map((userId) => ({ user_id: userId })),
151
+ nextCursor: response.response_metadata?.next_cursor || void 0
152
+ };
153
+ }
154
+ async function listThreadReplies(input) {
155
+ const client = getSlackClient();
156
+ const channelId = normalizeSlackConversationId(input.channelId);
157
+ if (!channelId) {
158
+ throw new Error("Slack thread reply lookup requires a valid channel ID");
159
+ }
160
+ const targetLimit = Math.max(1, Math.min(input.limit ?? 1e3, 1e3));
161
+ const maxPages = Math.max(1, Math.min(input.maxPages ?? 10, 10));
162
+ const pendingTargets = new Set(
163
+ (input.targetMessageTs ?? []).filter((value) => typeof value === "string" && value.length > 0)
164
+ );
165
+ const replies = [];
166
+ let cursor;
167
+ let pages = 0;
168
+ while (replies.length < targetLimit && pages < maxPages) {
169
+ pages += 1;
170
+ const pageLimit = Math.max(1, Math.min(200, targetLimit - replies.length));
171
+ const response = await withSlackRetries(
172
+ () => client.conversations.replies({
173
+ channel: channelId,
174
+ ts: input.threadTs,
175
+ limit: pageLimit,
176
+ cursor
177
+ }),
178
+ 3,
179
+ { action: "conversations.replies" }
180
+ );
181
+ const batch = response.messages ?? [];
182
+ replies.push(...batch);
183
+ for (const reply of batch) {
184
+ if (typeof reply.ts === "string" && pendingTargets.size > 0) {
185
+ pendingTargets.delete(reply.ts);
186
+ }
187
+ }
188
+ cursor = response.response_metadata?.next_cursor || void 0;
189
+ if (!cursor || pendingTargets.size === 0) {
190
+ break;
191
+ }
192
+ }
193
+ return replies.slice(0, targetLimit);
194
+ }
195
+
196
+ export {
197
+ postMessageToChannel,
198
+ addReactionToMessage,
199
+ removeReactionFromMessage,
200
+ listChannelMessages,
201
+ listChannelMembers,
202
+ listThreadReplies
203
+ };
@@ -0,0 +1,95 @@
1
+ import {
2
+ createRequestContext,
3
+ logException,
4
+ logWarn,
5
+ setSpanAttributes,
6
+ setSpanStatus,
7
+ withContext,
8
+ withSpan
9
+ } from "./chunk-BBOVH5RF.js";
10
+
11
+ // src/handlers/webhooks.ts
12
+ import { after } from "next/server";
13
+ import * as Sentry from "@sentry/nextjs";
14
+ async function loadBot() {
15
+ const { bot } = await import("./bot-DLML4Z7F.js");
16
+ return bot;
17
+ }
18
+ async function POST(request, context) {
19
+ const bot = await loadBot();
20
+ const { platform } = await context.params;
21
+ const handler = bot.webhooks[platform];
22
+ const requestContext = createRequestContext(request, { platform });
23
+ const requestUrl = new URL(request.url);
24
+ return withContext(requestContext, async () => {
25
+ if (!handler) {
26
+ const error = new Error(`Unknown platform: ${platform}`);
27
+ logException(error, "webhook_platform_unknown", {}, {
28
+ "http.response.status_code": 404
29
+ }, `Unknown platform: ${platform}`);
30
+ return new Response(`Unknown platform: ${platform}`, { status: 404 });
31
+ }
32
+ try {
33
+ return await withSpan(
34
+ "http.server.request",
35
+ "http.server",
36
+ requestContext,
37
+ async () => {
38
+ try {
39
+ const activeSpan = Sentry.getActiveSpan();
40
+ const response = await handler(request, {
41
+ waitUntil: (task) => after(() => {
42
+ const runTask = () => {
43
+ const taskOrFactory = task;
44
+ return typeof taskOrFactory === "function" ? taskOrFactory() : taskOrFactory;
45
+ };
46
+ if (activeSpan) {
47
+ return Sentry.withActiveSpan(activeSpan, runTask);
48
+ }
49
+ return runTask();
50
+ })
51
+ });
52
+ if (response.status >= 400) {
53
+ let responseBodySnippet;
54
+ try {
55
+ responseBodySnippet = (await response.clone().text()).slice(0, 300);
56
+ } catch {
57
+ responseBodySnippet = void 0;
58
+ }
59
+ logWarn(
60
+ "webhook_non_success_response",
61
+ {},
62
+ {
63
+ "http.response.status_code": response.status,
64
+ "http.request.header.x_slack_signature": request.headers.get("x-slack-signature") ?? void 0,
65
+ "http.request.header.x_slack_request_timestamp": request.headers.get("x-slack-request-timestamp") ?? void 0,
66
+ ...responseBodySnippet ? { "app.webhook.response_body": responseBodySnippet } : {}
67
+ },
68
+ `Webhook ${platform} returned ${response.status}`
69
+ );
70
+ }
71
+ setSpanAttributes({
72
+ "http.response.status_code": response.status
73
+ });
74
+ setSpanStatus(response.status >= 500 ? "error" : "ok");
75
+ return response;
76
+ } catch (error) {
77
+ setSpanStatus("error");
78
+ throw error;
79
+ }
80
+ },
81
+ {
82
+ "http.request.method": request.method,
83
+ "url.path": requestUrl.pathname
84
+ }
85
+ );
86
+ } catch (error) {
87
+ logException(error, "webhook_handler_failed");
88
+ throw error;
89
+ }
90
+ });
91
+ }
92
+
93
+ export {
94
+ POST
95
+ };
@@ -0,0 +1,39 @@
1
+ // src/chat/queue/client.ts
2
+ import { handleCallback, send } from "@vercel/queue";
3
+ var DEFAULT_TOPIC_NAME = "junior-thread-message";
4
+ var MAX_DELIVERY_ATTEMPTS = 10;
5
+ function getThreadMessageTopic() {
6
+ return DEFAULT_TOPIC_NAME;
7
+ }
8
+ async function enqueueThreadMessage(payload, options) {
9
+ const result = await send(getThreadMessageTopic(), payload, {
10
+ ...options?.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}
11
+ });
12
+ return result.messageId ?? void 0;
13
+ }
14
+ function createQueueCallbackHandler(handler) {
15
+ return handleCallback(
16
+ async (message, metadata) => {
17
+ await handler(message, {
18
+ messageId: metadata.messageId,
19
+ deliveryCount: metadata.deliveryCount,
20
+ topicName: metadata.topicName
21
+ });
22
+ },
23
+ {
24
+ retry: (_error, metadata) => {
25
+ if (metadata.deliveryCount >= MAX_DELIVERY_ATTEMPTS) {
26
+ return { acknowledge: true };
27
+ }
28
+ const backoffSeconds = Math.min(300, Math.max(5, metadata.deliveryCount * 5));
29
+ return { afterSeconds: backoffSeconds };
30
+ }
31
+ }
32
+ );
33
+ }
34
+
35
+ export {
36
+ getThreadMessageTopic,
37
+ enqueueThreadMessage,
38
+ createQueueCallbackHandler
39
+ };