@sentry/junior 0.1.0 → 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.
@@ -0,0 +1,272 @@
1
+ import {
2
+ appSlackRuntime,
3
+ createQueueCallbackHandler,
4
+ downloadPrivateSlackFile,
5
+ getThreadMessageTopic,
6
+ removeReactionFromMessage
7
+ } from "./chunk-RXNMJQPY.js";
8
+ import {
9
+ acquireQueueMessageProcessingOwnership,
10
+ completeQueueMessageProcessingOwnership,
11
+ failQueueMessageProcessingOwnership,
12
+ getQueueMessageProcessingState,
13
+ getStateAdapter,
14
+ refreshQueueMessageProcessingOwnership
15
+ } from "./chunk-QHDDCUTN.js";
16
+ import {
17
+ createRequestContext,
18
+ logError,
19
+ logException,
20
+ logWarn,
21
+ setSpanStatus,
22
+ withContext,
23
+ withSpan
24
+ } from "./chunk-PY4AI2GZ.js";
25
+
26
+ // src/chat/queue/process-thread-message.ts
27
+ import { Message, ThreadImpl } from "chat";
28
+
29
+ // src/chat/thread-runtime/process-thread-message-runtime.ts
30
+ function rehydrateAttachmentFetchers(payload) {
31
+ for (const attachment of payload.message.attachments) {
32
+ if (!attachment.fetchData && attachment.url) {
33
+ attachment.fetchData = () => downloadPrivateSlackFile(attachment.url);
34
+ }
35
+ }
36
+ }
37
+ async function processThreadMessageRuntime(args) {
38
+ const runtimePayload = {
39
+ message: args.message,
40
+ thread: args.thread
41
+ };
42
+ rehydrateAttachmentFetchers(runtimePayload);
43
+ if (args.kind === "new_mention") {
44
+ await appSlackRuntime.handleNewMention(args.thread, args.message, {
45
+ beforeFirstResponsePost: args.beforeFirstResponsePost
46
+ });
47
+ return;
48
+ }
49
+ if (args.kind === "subscribed_reply") {
50
+ await appSlackRuntime.handleSubscribedMessage(args.thread, args.message, {
51
+ beforeFirstResponsePost: args.beforeFirstResponsePost,
52
+ preApprovedReply: true
53
+ });
54
+ return;
55
+ }
56
+ await appSlackRuntime.handleSubscribedMessage(args.thread, args.message, {
57
+ beforeFirstResponsePost: args.beforeFirstResponsePost
58
+ });
59
+ }
60
+
61
+ // src/chat/queue/process-thread-message.ts
62
+ var stateAdapterConnected = false;
63
+ function isSerializedThread(thread) {
64
+ return typeof thread === "object" && thread !== null && thread._type === "chat:Thread";
65
+ }
66
+ function isSerializedMessage(message) {
67
+ return typeof message === "object" && message !== null && message._type === "chat:Message";
68
+ }
69
+ function getPayloadChannelId(payload) {
70
+ return payload.thread.channelId;
71
+ }
72
+ function getPayloadUserId(payload) {
73
+ return payload.message.author?.userId;
74
+ }
75
+ function createMessageOwnerToken() {
76
+ return `msg-${Date.now()}-${Math.random().toString(16).slice(2)}`;
77
+ }
78
+ var QueueMessageOwnershipError = class extends Error {
79
+ constructor(stage, dedupKey) {
80
+ super(`Queue message ownership lost during ${stage} for dedupKey=${dedupKey}`);
81
+ this.name = "QueueMessageOwnershipError";
82
+ }
83
+ };
84
+ var defaultProcessQueuedThreadMessageDeps = {
85
+ clearProcessingReaction: async ({ channelId, timestamp }) => {
86
+ await removeReactionFromMessage({
87
+ channelId,
88
+ timestamp,
89
+ emoji: "eyes"
90
+ });
91
+ },
92
+ logWarn,
93
+ processRuntime: processThreadMessageRuntime
94
+ };
95
+ function deserializeThread(thread) {
96
+ if (isSerializedThread(thread)) {
97
+ return ThreadImpl.fromJSON(thread);
98
+ }
99
+ return thread;
100
+ }
101
+ function deserializeMessage(message) {
102
+ if (isSerializedMessage(message)) {
103
+ return Message.fromJSON(message);
104
+ }
105
+ return message;
106
+ }
107
+ async function logThreadMessageFailure(payload, errorMessage) {
108
+ logError(
109
+ "queue_message_failed",
110
+ {
111
+ slackThreadId: payload.normalizedThreadId,
112
+ slackChannelId: getPayloadChannelId(payload),
113
+ slackUserId: getPayloadUserId(payload)
114
+ },
115
+ {
116
+ "messaging.message.id": payload.message.id,
117
+ "app.queue.message_kind": payload.kind,
118
+ "app.queue.message_id": payload.queueMessageId,
119
+ "error.message": errorMessage
120
+ },
121
+ "Queue message processing failed"
122
+ );
123
+ }
124
+ async function processQueuedThreadMessage(payload, deps = defaultProcessQueuedThreadMessageDeps) {
125
+ const existingMessageState = await getQueueMessageProcessingState(payload.dedupKey);
126
+ if (existingMessageState?.status === "completed") {
127
+ return;
128
+ }
129
+ const ownerToken = createMessageOwnerToken();
130
+ const claimResult = await acquireQueueMessageProcessingOwnership({
131
+ rawKey: payload.dedupKey,
132
+ ownerToken,
133
+ queueMessageId: payload.queueMessageId
134
+ });
135
+ if (claimResult === "blocked") {
136
+ return;
137
+ }
138
+ const threadWasSerialized = isSerializedThread(payload.thread);
139
+ if (threadWasSerialized && !stateAdapterConnected) {
140
+ await getStateAdapter().connect();
141
+ stateAdapterConnected = true;
142
+ }
143
+ const runtimePayload = {
144
+ ...payload,
145
+ thread: deserializeThread(payload.thread),
146
+ message: deserializeMessage(payload.message)
147
+ };
148
+ try {
149
+ const refreshed = await refreshQueueMessageProcessingOwnership({
150
+ rawKey: payload.dedupKey,
151
+ ownerToken,
152
+ queueMessageId: payload.queueMessageId
153
+ });
154
+ if (!refreshed) {
155
+ throw new QueueMessageOwnershipError("refresh", payload.dedupKey);
156
+ }
157
+ let reactionCleared = false;
158
+ const clearReactionBeforeFirstResponsePost = async () => {
159
+ if (reactionCleared) {
160
+ return;
161
+ }
162
+ reactionCleared = true;
163
+ try {
164
+ await deps.clearProcessingReaction({
165
+ channelId: runtimePayload.thread.channelId,
166
+ timestamp: runtimePayload.message.id
167
+ });
168
+ } catch (error) {
169
+ const errorMessage = error instanceof Error ? error.message : String(error);
170
+ deps.logWarn(
171
+ "queue_processing_reaction_clear_failed",
172
+ {
173
+ slackThreadId: payload.normalizedThreadId,
174
+ slackChannelId: getPayloadChannelId(payload),
175
+ slackUserId: getPayloadUserId(payload)
176
+ },
177
+ {
178
+ "messaging.message.id": payload.message.id,
179
+ "app.queue.message_kind": payload.kind,
180
+ "app.queue.message_id": payload.queueMessageId,
181
+ "error.message": errorMessage
182
+ },
183
+ "Failed to remove processing reaction before sending queue response"
184
+ );
185
+ }
186
+ };
187
+ await deps.processRuntime({
188
+ kind: runtimePayload.kind,
189
+ thread: runtimePayload.thread,
190
+ message: runtimePayload.message,
191
+ beforeFirstResponsePost: clearReactionBeforeFirstResponsePost
192
+ });
193
+ const completed = await completeQueueMessageProcessingOwnership({
194
+ rawKey: payload.dedupKey,
195
+ ownerToken,
196
+ queueMessageId: payload.queueMessageId
197
+ });
198
+ if (!completed) {
199
+ throw new QueueMessageOwnershipError("complete", payload.dedupKey);
200
+ }
201
+ } catch (error) {
202
+ const errorMessage = error instanceof Error ? error.message : String(error);
203
+ await logThreadMessageFailure(payload, errorMessage);
204
+ const failed = await failQueueMessageProcessingOwnership({
205
+ rawKey: payload.dedupKey,
206
+ ownerToken,
207
+ errorMessage,
208
+ queueMessageId: payload.queueMessageId
209
+ });
210
+ if (!failed && !(error instanceof QueueMessageOwnershipError)) {
211
+ throw new Error(`Failed to persist queue message failure state for dedupKey=${payload.dedupKey}: ${errorMessage}`);
212
+ }
213
+ throw error;
214
+ }
215
+ }
216
+
217
+ // src/handlers/queue-callback.ts
218
+ var callbackHandler = createQueueCallbackHandler(async (message, metadata) => {
219
+ if (metadata.topicName === getThreadMessageTopic()) {
220
+ const payload = {
221
+ ...message,
222
+ queueMessageId: metadata.messageId
223
+ };
224
+ await withSpan(
225
+ "queue.process_message",
226
+ "queue.process_message",
227
+ {
228
+ slackThreadId: payload.normalizedThreadId,
229
+ slackChannelId: payload.thread.channelId,
230
+ slackUserId: payload.message.author?.userId
231
+ },
232
+ async () => {
233
+ await processQueuedThreadMessage(payload);
234
+ },
235
+ {
236
+ "messaging.message.id": payload.message.id,
237
+ "app.queue.message_kind": payload.kind,
238
+ "app.queue.message_id": payload.queueMessageId,
239
+ "app.queue.delivery_count": metadata.deliveryCount,
240
+ "app.queue.topic": metadata.topicName
241
+ }
242
+ );
243
+ return;
244
+ }
245
+ throw new Error(`Unexpected queue topic: ${metadata.topicName}`);
246
+ });
247
+ async function POST(request) {
248
+ const requestContext = createRequestContext(request, { platform: "queue" });
249
+ return withContext(requestContext, async () => {
250
+ try {
251
+ const response = await callbackHandler(request);
252
+ setSpanStatus(response.status >= 500 ? "error" : "ok");
253
+ return response;
254
+ } catch (error) {
255
+ const message = error instanceof Error ? error.message : String(error);
256
+ logError(
257
+ "queue_callback_failed",
258
+ {},
259
+ {
260
+ "error.message": message
261
+ },
262
+ "Queue callback processing failed"
263
+ );
264
+ logException(error, "queue_callback_failed");
265
+ throw error;
266
+ }
267
+ });
268
+ }
269
+
270
+ export {
271
+ POST
272
+ };
@@ -0,0 +1,3 @@
1
+ declare function runInit(dir: string, log?: (line: string) => void): Promise<void>;
2
+
3
+ export { runInit };
@@ -0,0 +1,105 @@
1
+ // src/cli/init.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ function writeRouteModule(filePath, exportLine) {
5
+ fs.writeFileSync(filePath, `${exportLine}
6
+ export const runtime = "nodejs";
7
+ `);
8
+ }
9
+ function writeWrapperFiles(targetDir) {
10
+ const routeDir = path.join(targetDir, "app", "api", "[...path]");
11
+ fs.mkdirSync(routeDir, { recursive: true });
12
+ writeRouteModule(path.join(routeDir, "route.js"), 'export { GET, POST } from "@sentry/junior/handler";');
13
+ const queueRouteDir = path.join(targetDir, "app", "api", "queue", "callback");
14
+ fs.mkdirSync(queueRouteDir, { recursive: true });
15
+ writeRouteModule(
16
+ path.join(queueRouteDir, "route.js"),
17
+ 'export { POST } from "@sentry/junior/handlers/queue-callback";'
18
+ );
19
+ fs.mkdirSync(path.join(targetDir, "app"), { recursive: true });
20
+ fs.writeFileSync(
21
+ path.join(targetDir, "app", "layout.js"),
22
+ 'export { default } from "@sentry/junior/app/layout";\n'
23
+ );
24
+ fs.writeFileSync(
25
+ path.join(targetDir, "next.config.mjs"),
26
+ 'import { withJunior } from "@sentry/junior/config";\nexport default withJunior();\n'
27
+ );
28
+ fs.writeFileSync(
29
+ path.join(targetDir, "instrumentation.js"),
30
+ 'export { register, onRequestError } from "@sentry/junior/instrumentation";\n'
31
+ );
32
+ }
33
+ async function runInit(dir, log = console.log) {
34
+ const targetDir = dir.trim();
35
+ if (!targetDir) {
36
+ throw new Error("usage: junior init <dir>");
37
+ }
38
+ const target = path.resolve(targetDir);
39
+ if (fs.existsSync(target)) {
40
+ const stat = fs.statSync(target);
41
+ if (!stat.isDirectory()) {
42
+ throw new Error(`refusing to initialize non-directory path: ${target}`);
43
+ }
44
+ if (fs.readdirSync(target).length > 0) {
45
+ throw new Error(`refusing to initialize non-empty directory: ${target}`);
46
+ }
47
+ } else {
48
+ fs.mkdirSync(target, { recursive: true });
49
+ }
50
+ const name = path.basename(target);
51
+ const pkg = {
52
+ name,
53
+ version: "0.1.0",
54
+ private: true,
55
+ type: "module",
56
+ scripts: {
57
+ dev: "next dev",
58
+ build: "next build",
59
+ start: "next start"
60
+ },
61
+ dependencies: {
62
+ "@sentry/junior": "latest",
63
+ next: "^16.0.0",
64
+ react: "^19.0.0",
65
+ "react-dom": "^19.0.0",
66
+ "@sentry/nextjs": "^10.0.0"
67
+ }
68
+ };
69
+ fs.writeFileSync(path.join(target, "package.json"), `${JSON.stringify(pkg, null, 2)}
70
+ `);
71
+ const dataDir = path.join(target, "app", "data");
72
+ fs.mkdirSync(dataDir, { recursive: true });
73
+ fs.writeFileSync(path.join(dataDir, "SOUL.md"), `# ${name}
74
+
75
+ You are ${name}, a helpful assistant.
76
+ `);
77
+ const skillsDir = path.join(target, "app", "skills");
78
+ fs.mkdirSync(skillsDir, { recursive: true });
79
+ fs.writeFileSync(path.join(skillsDir, ".gitkeep"), "");
80
+ const pluginsDir = path.join(target, "app", "plugins");
81
+ fs.mkdirSync(pluginsDir, { recursive: true });
82
+ fs.writeFileSync(path.join(pluginsDir, ".gitkeep"), "");
83
+ fs.writeFileSync(path.join(target, ".gitignore"), ["node_modules/", ".next/", ".env", ".env.local", ""].join("\n"));
84
+ fs.writeFileSync(
85
+ path.join(target, ".env.example"),
86
+ [
87
+ "SLACK_BOT_TOKEN=",
88
+ "SLACK_SIGNING_SECRET=",
89
+ "JUNIOR_BOT_NAME=",
90
+ "AI_MODEL=",
91
+ "AI_FAST_MODEL=",
92
+ "REDIS_URL=",
93
+ "NEXT_PUBLIC_SENTRY_DSN=",
94
+ ""
95
+ ].join("\n")
96
+ );
97
+ writeWrapperFiles(target);
98
+ log(`Created ${name} at ${target}`);
99
+ log("");
100
+ log(` cd ${targetDir} && pnpm install && pnpm dev`);
101
+ log("");
102
+ }
103
+ export {
104
+ runInit
105
+ };
@@ -0,0 +1,11 @@
1
+ declare const CLI_USAGE = "usage: junior init <dir>\n junior snapshot create";
2
+ interface CliHandlers {
3
+ runInit: (dir: string) => Promise<void>;
4
+ runSnapshotCreate: () => Promise<void>;
5
+ }
6
+ interface CliIo {
7
+ error: (line: string) => void;
8
+ }
9
+ declare function runCli(argv: string[], handlers: CliHandlers, io?: CliIo): Promise<number>;
10
+
11
+ export { CLI_USAGE, runCli };
@@ -0,0 +1,30 @@
1
+ // src/cli/run.ts
2
+ var CLI_USAGE = "usage: junior init <dir>\n junior snapshot create";
3
+ var DEFAULT_IO = {
4
+ error: console.error
5
+ };
6
+ async function runCli(argv, handlers, io = DEFAULT_IO) {
7
+ const [command, subcommand, ...rest] = argv;
8
+ if (command === "init") {
9
+ if (!subcommand || rest.length > 0) {
10
+ io.error(CLI_USAGE);
11
+ return 1;
12
+ }
13
+ await handlers.runInit(subcommand);
14
+ return 0;
15
+ }
16
+ if (command === "snapshot" && subcommand === "create") {
17
+ if (rest.length > 0) {
18
+ io.error(CLI_USAGE);
19
+ return 1;
20
+ }
21
+ await handlers.runSnapshotCreate();
22
+ return 0;
23
+ }
24
+ io.error(CLI_USAGE);
25
+ return 1;
26
+ }
27
+ export {
28
+ CLI_USAGE,
29
+ runCli
30
+ };
@@ -0,0 +1,3 @@
1
+ declare function runSnapshotCreate(log?: (line: string) => void): Promise<void>;
2
+
3
+ export { runSnapshotCreate };
@@ -0,0 +1,57 @@
1
+ import {
2
+ disconnectStateAdapter,
3
+ resolveRuntimeDependencySnapshot
4
+ } from "../chunk-QHDDCUTN.js";
5
+ import "../chunk-PY4AI2GZ.js";
6
+
7
+ // src/cli/snapshot-warmup.ts
8
+ var DEFAULT_RUNTIME = "node22";
9
+ var DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
10
+ function progressMessage(phase) {
11
+ if (phase === "resolve_start") {
12
+ return "Resolving sandbox snapshot profile...";
13
+ }
14
+ if (phase === "cache_hit") {
15
+ return "Using cached sandbox snapshot.";
16
+ }
17
+ if (phase === "waiting_for_lock") {
18
+ return "Waiting for sandbox snapshot build lock...";
19
+ }
20
+ if (phase === "building_snapshot") {
21
+ return "Building sandbox snapshot...";
22
+ }
23
+ return "Sandbox snapshot build complete.";
24
+ }
25
+ async function runSnapshotCreate(log = console.log) {
26
+ const runtime = DEFAULT_RUNTIME;
27
+ const timeoutMs = DEFAULT_TIMEOUT_MS;
28
+ try {
29
+ const emitted = /* @__PURE__ */ new Set();
30
+ const snapshot = await resolveRuntimeDependencySnapshot({
31
+ runtime,
32
+ timeoutMs,
33
+ onProgress: async (phase) => {
34
+ if (emitted.has(phase)) {
35
+ return;
36
+ }
37
+ emitted.add(phase);
38
+ log(progressMessage(phase));
39
+ }
40
+ });
41
+ const fields = [
42
+ `runtime=${runtime}`,
43
+ `resolve_outcome=${snapshot.resolveOutcome}`,
44
+ `cache_hit=${snapshot.cacheHit}`,
45
+ `dependency_count=${snapshot.dependencyCount}`,
46
+ ...snapshot.profileHash ? [`profile_hash=${snapshot.profileHash}`] : [],
47
+ ...snapshot.snapshotId ? [`snapshot_id=${snapshot.snapshotId}`] : [],
48
+ ...snapshot.rebuildReason ? [`rebuild_reason=${snapshot.rebuildReason}`] : []
49
+ ];
50
+ log(`Sandbox snapshot create complete: ${fields.join(" ")}`);
51
+ } finally {
52
+ await disconnectStateAdapter();
53
+ }
54
+ }
55
+ export {
56
+ runSnapshotCreate
57
+ };
@@ -1,3 +1,6 @@
1
+ /**
2
+ * Returns a minimal JSON health response for runtime health checks.
3
+ */
1
4
  declare function GET(): Promise<Response>;
2
5
 
3
6
  export { GET };
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Handles `POST /api/queue/callback` for asynchronous thread processing.
3
+ *
4
+ * Keep this route as a dedicated handler in app code. The catch-all router can
5
+ * mirror this path for local/dev parity, but production queue delivery should
6
+ * always target the dedicated endpoint.
7
+ */
1
8
  declare function POST(request: Request): Promise<Response>;
2
9
 
3
10
  export { POST };