@sentry/junior 0.1.1 → 0.3.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 +10 -165
- package/bin/junior.mjs +38 -128
- package/dist/app/layout.d.ts +3 -0
- package/dist/{bot-6KXJ366H.js → bot-JSIREVQD.js} +4 -3
- package/dist/chunk-4RFOJSJL.js +272 -0
- package/dist/chunk-DPTR2FNH.js +1866 -0
- package/dist/{chunk-5LLCJPTH.js → chunk-L745IWNK.js} +7132 -5459
- package/dist/{chunk-BBOVH5RF.js → chunk-PY4AI2GZ.js} +62 -15
- package/dist/{chunk-OVG2HBNM.js → chunk-RTQMRGZD.js} +2 -2
- package/dist/chunk-SP6LV35L.js +328 -0
- package/dist/cli/init.d.ts +3 -0
- package/dist/cli/init.js +105 -0
- package/dist/cli/run.d.ts +11 -0
- package/dist/cli/run.js +30 -0
- package/dist/cli/snapshot-warmup.d.ts +3 -0
- package/dist/cli/snapshot-warmup.js +58 -0
- package/dist/handlers/health.d.ts +3 -0
- package/dist/handlers/queue-callback.d.ts +7 -0
- package/dist/handlers/queue-callback.js +6 -266
- package/dist/handlers/router.d.ts +18 -3
- package/dist/handlers/router.js +321 -13
- package/dist/handlers/webhooks.d.ts +13 -0
- package/dist/handlers/webhooks.js +2 -2
- package/dist/instrumentation.d.ts +6 -0
- package/dist/next-config.d.ts +10 -2
- package/dist/next-config.js +58 -57
- package/package.json +3 -8
- package/dist/chunk-CJFEZLEN.js +0 -3136
- package/dist/route-DMVINKJW.js +0 -304
package/README.md
CHANGED
|
@@ -2,11 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
`@sentry/junior` is a Slack bot package for Next.js apps.
|
|
4
4
|
|
|
5
|
-
If you are contributing to this monorepo, use the root docs:
|
|
6
|
-
|
|
7
|
-
- `README.md` for general usage
|
|
8
|
-
- `CONTRIBUTING.md` for development workflows
|
|
9
|
-
|
|
10
5
|
## Install
|
|
11
6
|
|
|
12
7
|
```bash
|
|
@@ -14,18 +9,7 @@ pnpm add @sentry/junior
|
|
|
14
9
|
pnpm add next react react-dom @sentry/nextjs
|
|
15
10
|
```
|
|
16
11
|
|
|
17
|
-
##
|
|
18
|
-
|
|
19
|
-
Add these files under `app/`:
|
|
20
|
-
|
|
21
|
-
```text
|
|
22
|
-
app/SOUL.md
|
|
23
|
-
app/ABOUT.md
|
|
24
|
-
app/skills/
|
|
25
|
-
app/plugins/ (optional)
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Next.js Integration
|
|
12
|
+
## Quick usage
|
|
29
13
|
|
|
30
14
|
`app/api/[...path]/route.js`:
|
|
31
15
|
|
|
@@ -45,156 +29,17 @@ export const runtime = "nodejs";
|
|
|
45
29
|
|
|
46
30
|
```js
|
|
47
31
|
import { withJunior } from "@sentry/junior/config";
|
|
48
|
-
export default withJunior();
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
`instrumentation.js`:
|
|
52
32
|
|
|
53
|
-
|
|
54
|
-
|
|
33
|
+
export default withJunior({
|
|
34
|
+
pluginPackages: ["@sentry/junior-github", "@sentry/junior-sentry"]
|
|
35
|
+
});
|
|
55
36
|
```
|
|
56
37
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
`app/layout.js`:
|
|
60
|
-
|
|
61
|
-
```js
|
|
62
|
-
export { default } from "@sentry/junior/app/layout";
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
## Scaffold a New Bot
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
npx junior init my-bot
|
|
69
|
-
cd my-bot
|
|
70
|
-
pnpm install
|
|
71
|
-
pnpm dev
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Deploy on Vercel
|
|
75
|
-
|
|
76
|
-
Use this flow for a production-ready setup.
|
|
77
|
-
|
|
78
|
-
### 1) Create and link a Vercel project
|
|
79
|
-
|
|
80
|
-
Dashboard: `Vercel -> Add New... -> Project`, then import your repo.
|
|
81
|
-
|
|
82
|
-
CLI:
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
pnpm dlx vercel@latest login
|
|
86
|
-
pnpm dlx vercel@latest link
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
### 2) Configure required app routes in your Next.js app
|
|
90
|
-
|
|
91
|
-
Ensure you have these files:
|
|
92
|
-
|
|
93
|
-
`app/api/[...path]/route.js`:
|
|
94
|
-
|
|
95
|
-
```js
|
|
96
|
-
export { GET, POST } from "@sentry/junior/handler";
|
|
97
|
-
export const runtime = "nodejs";
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
`app/api/queue/callback/route.js`:
|
|
101
|
-
|
|
102
|
-
```js
|
|
103
|
-
export { POST } from "@sentry/junior/handlers/queue-callback";
|
|
104
|
-
export const runtime = "nodejs";
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### 3) Configure Vercel Queue
|
|
108
|
-
|
|
109
|
-
Dashboard: `Project -> Settings -> Functions` (verify queue trigger after deploy).
|
|
110
|
-
|
|
111
|
-
Add this `vercel.json` trigger:
|
|
112
|
-
|
|
113
|
-
```json
|
|
114
|
-
{
|
|
115
|
-
"functions": {
|
|
116
|
-
"app/api/queue/callback/route.js": {
|
|
117
|
-
"experimentalTriggers": [
|
|
118
|
-
{
|
|
119
|
-
"type": "queue/v2beta",
|
|
120
|
-
"topic": "junior-thread-message"
|
|
121
|
-
}
|
|
122
|
-
]
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### 4) Configure Redis (`REDIS_URL`)
|
|
129
|
-
|
|
130
|
-
Dashboard options:
|
|
131
|
-
|
|
132
|
-
- Add a Redis integration from `Project -> Storage` (for example Upstash Redis), or
|
|
133
|
-
- Use any external Redis provider and copy its connection URL.
|
|
134
|
-
|
|
135
|
-
Then set `REDIS_URL` in Vercel env vars.
|
|
136
|
-
|
|
137
|
-
### 5) Configure environment variables
|
|
138
|
-
|
|
139
|
-
Set production env vars first, then repeat for `preview` and `development` as needed.
|
|
140
|
-
|
|
141
|
-
#### Required
|
|
142
|
-
|
|
143
|
-
- `SLACK_SIGNING_SECRET`
|
|
144
|
-
- `SLACK_BOT_TOKEN` (or `SLACK_BOT_USER_TOKEN`)
|
|
145
|
-
- `REDIS_URL`
|
|
146
|
-
|
|
147
|
-
CLI:
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
vercel env add SLACK_SIGNING_SECRET production --sensitive
|
|
151
|
-
vercel env add SLACK_BOT_TOKEN production --sensitive
|
|
152
|
-
vercel env add REDIS_URL production --sensitive
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
#### Recommended
|
|
156
|
-
|
|
157
|
-
- `JUNIOR_BOT_NAME` (defaults to `junior`)
|
|
158
|
-
- `AI_MODEL` (defaults to `anthropic/claude-sonnet-4.6`)
|
|
159
|
-
- `AI_FAST_MODEL` (defaults to `anthropic/claude-haiku-4.5`)
|
|
160
|
-
|
|
161
|
-
CLI:
|
|
162
|
-
|
|
163
|
-
```bash
|
|
164
|
-
vercel env add JUNIOR_BOT_NAME production
|
|
165
|
-
vercel env add AI_MODEL production
|
|
166
|
-
vercel env add AI_FAST_MODEL production
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
#### Optional
|
|
170
|
-
|
|
171
|
-
- `JUNIOR_BASE_URL` (set if your canonical external URL differs from Vercel auto-resolved URLs; used for OAuth callback links)
|
|
172
|
-
- `AI_GATEWAY_API_KEY` (optional override; in Vercel runtime, ambient `VERCEL_OIDC_TOKEN` is used automatically for AI gateway auth)
|
|
173
|
-
|
|
174
|
-
After env changes, redeploy so the new deployment picks up updated values.
|
|
175
|
-
|
|
176
|
-
### 6) Configure Slack app (external prerequisite)
|
|
177
|
-
|
|
178
|
-
In Slack app settings:
|
|
179
|
-
|
|
180
|
-
1. Set request URL(s) to `https://<your-domain>/api/webhooks/slack` for Events and Interactivity.
|
|
181
|
-
2. Install the app to your workspace.
|
|
182
|
-
3. Copy credentials into Vercel env vars:
|
|
183
|
-
- Bot token -> `SLACK_BOT_TOKEN`
|
|
184
|
-
- Signing secret -> `SLACK_SIGNING_SECRET`
|
|
185
|
-
|
|
186
|
-
### 7) Deploy and verify
|
|
187
|
-
|
|
188
|
-
1. Deploy the app to Vercel.
|
|
189
|
-
2. Confirm health endpoint responds:
|
|
190
|
-
- `GET https://<your-domain>/api/health`
|
|
191
|
-
3. Mention the bot in Slack and confirm a threaded response arrives.
|
|
192
|
-
4. Confirm queue processing is active in logs (enqueue + callback processing).
|
|
193
|
-
5. Confirm there are no `REDIS_URL is required` runtime errors.
|
|
194
|
-
|
|
195
|
-
## Plugin Setup
|
|
38
|
+
## Full docs
|
|
196
39
|
|
|
197
|
-
|
|
40
|
+
Canonical docs: **https://junior.sentry.dev/**
|
|
198
41
|
|
|
199
|
-
-
|
|
200
|
-
-
|
|
42
|
+
- Quickstart: https://junior.sentry.dev/start-here/quickstart/
|
|
43
|
+
- Deployment: https://junior.sentry.dev/start-here/deploy/
|
|
44
|
+
- Plugin setup: https://junior.sentry.dev/extend/plugins-overview/
|
|
45
|
+
- API reference: https://junior.sentry.dev/reference/api/
|
package/bin/junior.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import fs from "node:fs";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { parseArgs } from "node:util";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
6
|
|
|
7
7
|
const { positionals } = parseArgs({
|
|
8
8
|
allowPositionals: true,
|
|
@@ -11,138 +11,48 @@ const { positionals } = parseArgs({
|
|
|
11
11
|
|
|
12
12
|
const command = positionals[0];
|
|
13
13
|
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// app/api/queue/callback/route.js
|
|
25
|
-
const queueRouteDir = path.join(targetDir, "app", "api", "queue", "callback");
|
|
26
|
-
fs.mkdirSync(queueRouteDir, { recursive: true });
|
|
27
|
-
fs.writeFileSync(
|
|
28
|
-
path.join(queueRouteDir, "route.js"),
|
|
29
|
-
'export { POST } from "@sentry/junior/handlers/queue-callback";\n' +
|
|
30
|
-
'export const runtime = "nodejs";\n'
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
// app/layout.js
|
|
34
|
-
fs.mkdirSync(path.join(targetDir, "app"), { recursive: true });
|
|
35
|
-
fs.writeFileSync(
|
|
36
|
-
path.join(targetDir, "app", "layout.js"),
|
|
37
|
-
'export { default } from "@sentry/junior/app/layout";\n'
|
|
38
|
-
);
|
|
14
|
+
async function loadCliFunction(moduleName, exportName, unavailableMessage) {
|
|
15
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
16
|
+
const modulePath = path.join(path.dirname(currentFile), "..", "dist", "cli", `${moduleName}.js`);
|
|
17
|
+
const moduleUrl = pathToFileURL(modulePath).href;
|
|
18
|
+
const loadedModule = await import(moduleUrl);
|
|
19
|
+
if (typeof loadedModule[exportName] !== "function") {
|
|
20
|
+
throw new Error(unavailableMessage);
|
|
21
|
+
}
|
|
22
|
+
return loadedModule[exportName];
|
|
23
|
+
}
|
|
39
24
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
25
|
+
async function runSnapshotCreate() {
|
|
26
|
+
const runSnapshotCreateFn = await loadCliFunction(
|
|
27
|
+
"snapshot-warmup",
|
|
28
|
+
"runSnapshotCreate",
|
|
29
|
+
"Snapshot create module is unavailable; reinstall @sentry/junior and retry."
|
|
45
30
|
);
|
|
31
|
+
await runSnapshotCreateFn();
|
|
32
|
+
}
|
|
46
33
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
'export { register, onRequestError } from "@sentry/junior/instrumentation";\n'
|
|
51
|
-
);
|
|
34
|
+
async function runInit(dir) {
|
|
35
|
+
const runInitFn = await loadCliFunction("init", "runInit", "Init module is unavailable; reinstall @sentry/junior and retry.");
|
|
36
|
+
await runInitFn(dir);
|
|
52
37
|
}
|
|
53
38
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
39
|
+
async function main() {
|
|
40
|
+
const runCli = await loadCliFunction(
|
|
41
|
+
"run",
|
|
42
|
+
"runCli",
|
|
43
|
+
"CLI dispatcher module is unavailable; reinstall @sentry/junior and retry."
|
|
44
|
+
);
|
|
45
|
+
const exitCode = await runCli(positionals, {
|
|
46
|
+
runInit,
|
|
47
|
+
runSnapshotCreate
|
|
48
|
+
});
|
|
49
|
+
if (exitCode !== 0) {
|
|
50
|
+
process.exit(exitCode);
|
|
62
51
|
}
|
|
63
|
-
|
|
64
|
-
const target = path.resolve(dir);
|
|
65
|
-
fs.mkdirSync(target, { recursive: true });
|
|
66
|
-
|
|
67
|
-
const name = path.basename(target);
|
|
68
|
-
|
|
69
|
-
// package.json
|
|
70
|
-
const pkg = {
|
|
71
|
-
name,
|
|
72
|
-
version: "0.1.0",
|
|
73
|
-
private: true,
|
|
74
|
-
type: "module",
|
|
75
|
-
scripts: {
|
|
76
|
-
dev: "next dev",
|
|
77
|
-
build: "next build",
|
|
78
|
-
start: "next start"
|
|
79
|
-
},
|
|
80
|
-
dependencies: {
|
|
81
|
-
"@sentry/junior": "latest",
|
|
82
|
-
next: "^16.0.0",
|
|
83
|
-
react: "^19.0.0",
|
|
84
|
-
"react-dom": "^19.0.0",
|
|
85
|
-
"@sentry/nextjs": "^10.0.0"
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
fs.writeFileSync(
|
|
89
|
-
path.join(target, "package.json"),
|
|
90
|
-
JSON.stringify(pkg, null, 2) + "\n"
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
// app/data/SOUL.md
|
|
94
|
-
const dataDir = path.join(target, "app", "data");
|
|
95
|
-
fs.mkdirSync(dataDir, { recursive: true });
|
|
96
|
-
fs.writeFileSync(
|
|
97
|
-
path.join(dataDir, "SOUL.md"),
|
|
98
|
-
`# ${name}\n\nYou are ${name}, a helpful assistant.\n`
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
// app/skills/
|
|
102
|
-
const skillsDir = path.join(target, "app", "skills");
|
|
103
|
-
fs.mkdirSync(skillsDir, { recursive: true });
|
|
104
|
-
fs.writeFileSync(path.join(skillsDir, ".gitkeep"), "");
|
|
105
|
-
|
|
106
|
-
// app/plugins/
|
|
107
|
-
const pluginsDir = path.join(target, "app", "plugins");
|
|
108
|
-
fs.mkdirSync(pluginsDir, { recursive: true });
|
|
109
|
-
fs.writeFileSync(path.join(pluginsDir, ".gitkeep"), "");
|
|
110
|
-
|
|
111
|
-
// .gitignore
|
|
112
|
-
fs.writeFileSync(
|
|
113
|
-
path.join(target, ".gitignore"),
|
|
114
|
-
[
|
|
115
|
-
"node_modules/",
|
|
116
|
-
".next/",
|
|
117
|
-
".env",
|
|
118
|
-
".env.local",
|
|
119
|
-
""
|
|
120
|
-
].join("\n")
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
// .env.example
|
|
124
|
-
fs.writeFileSync(
|
|
125
|
-
path.join(target, ".env.example"),
|
|
126
|
-
[
|
|
127
|
-
"SLACK_BOT_TOKEN=",
|
|
128
|
-
"SLACK_SIGNING_SECRET=",
|
|
129
|
-
"JUNIOR_BOT_NAME=",
|
|
130
|
-
"AI_MODEL=",
|
|
131
|
-
"AI_FAST_MODEL=",
|
|
132
|
-
"REDIS_URL=",
|
|
133
|
-
"NEXT_PUBLIC_SENTRY_DSN=",
|
|
134
|
-
""
|
|
135
|
-
].join("\n")
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
writeWrapperFiles(target);
|
|
139
|
-
|
|
140
|
-
console.log(`Created ${name} at ${target}`);
|
|
141
|
-
console.log();
|
|
142
|
-
console.log(` cd ${dir} && pnpm install && pnpm dev`);
|
|
143
|
-
console.log();
|
|
144
|
-
process.exit(0);
|
|
145
52
|
}
|
|
146
53
|
|
|
147
|
-
|
|
148
|
-
|
|
54
|
+
main().catch((error) => {
|
|
55
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
56
|
+
console.error(`junior command failed: ${message}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
});
|
package/dist/app/layout.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Minimal root layout export for apps that do not provide one yet.
|
|
6
|
+
*/
|
|
4
7
|
declare function RootLayout({ children }: {
|
|
5
8
|
children: ReactNode;
|
|
6
9
|
}): react_jsx_runtime.JSX.Element;
|
|
@@ -4,9 +4,10 @@ import {
|
|
|
4
4
|
createNormalizingStream,
|
|
5
5
|
resetBotDepsForTests,
|
|
6
6
|
setBotDepsForTests
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import "./chunk-
|
|
9
|
-
import "./chunk-
|
|
7
|
+
} from "./chunk-L745IWNK.js";
|
|
8
|
+
import "./chunk-DPTR2FNH.js";
|
|
9
|
+
import "./chunk-PY4AI2GZ.js";
|
|
10
|
+
import "./chunk-SP6LV35L.js";
|
|
10
11
|
export {
|
|
11
12
|
appSlackRuntime,
|
|
12
13
|
bot,
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appSlackRuntime,
|
|
3
|
+
createQueueCallbackHandler,
|
|
4
|
+
downloadPrivateSlackFile,
|
|
5
|
+
getThreadMessageTopic,
|
|
6
|
+
removeReactionFromMessage
|
|
7
|
+
} from "./chunk-L745IWNK.js";
|
|
8
|
+
import {
|
|
9
|
+
acquireQueueMessageProcessingOwnership,
|
|
10
|
+
completeQueueMessageProcessingOwnership,
|
|
11
|
+
failQueueMessageProcessingOwnership,
|
|
12
|
+
getQueueMessageProcessingState,
|
|
13
|
+
getStateAdapter,
|
|
14
|
+
refreshQueueMessageProcessingOwnership
|
|
15
|
+
} from "./chunk-DPTR2FNH.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
|
+
};
|