feat(platform): slash.invoke and bot spans with guildId (v0.8.424)

Add guildId/channelId to slash.invoke; bot.message and bot.install spans
with structured errors for Phase 461 observability.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Raven Scott
2026-05-24 01:23:26 -04:00
co-authored by Cursor
parent ee5c0b4357
commit 7960b26acc
2 changed files with 63 additions and 27 deletions
+2
View File
@@ -35,6 +35,8 @@ Expose a single `EventEmitter` API the UI drives over IPC (`apps/pearcord/index.
| `onboarded`, `identity`, `storagePath`, `db`, `dbPath` | Session state |
| `listGuilds()`, `loadGuild(guildId)`, `createGuild({ name, publicListing? })`, `joinInvite(code)` | Guild session |
**v0.8.424 (Phase 461):** `slash.invoke` span includes `guildId`/`channelId`; `bot.message` + `bot.install` spans with `guildId`. Bundle: `npm run test:phase461-slash-bots`. See [SLASH_COMMANDS.md](../../docs/SLASH_COMMANDS.md), [BOTS.md](../../docs/BOTS.md).
**v0.8.423 (Phase 460):** `embed.resolve` span uses active `guildId`/`channelId` fallback; `attachment.stage`/`attachment.read` re-verified. Bundle: `npm run test:phase460-attachments-embeds`. See [ATTACHMENTS.md](../../docs/ATTACHMENTS.md), [EMBEDS.md](../../docs/EMBEDS.md).
**v0.8.422 (Phase 459):** `emoji.upsert` + `sticker.create` spans include `guildId` + `span.fail`; `reaction.toggle` error log includes `guildId`/`channelId` (re-verified). Bundle: `npm run test:phase459-reactions-emojis`. See [REACTIONS.md](../../docs/REACTIONS.md), [EMOJI.md](../../docs/EMOJI.md).
+61 -27
View File
@@ -9754,18 +9754,30 @@ class PearcordPlatform extends EventEmitter {
}
async installBotFromToken (tokenInput) {
const result = verifyInstallToken(tokenInput, {
expectedGuildId: this.guild?.guild?.id || undefined
})
if (!result.ok) throw new Error(result.error || 'invalid bot token')
const bot = payloadToBotRow(result.payload, result.issuerPublicKey)
if (!this.guild?.guild || this.guild.guild.id !== bot.guildId) {
throw new Error('load the target server before installing a bot')
const guildId = this.guild?.guild?.id || null
const span = this.log.time('bot.install', { guildId })
try {
const result = verifyInstallToken(tokenInput, {
expectedGuildId: guildId || undefined
})
if (!result.ok) throw new Error(result.error || 'invalid bot token')
const bot = payloadToBotRow(result.payload, result.issuerPublicKey)
if (!this.guild?.guild || this.guild.guild.id !== bot.guildId) {
throw new Error('load the target server before installing a bot')
}
await this._ingestGuildBot(bot)
if (this.guild) this.guild.gossipBotInstall(bot)
this.emit('bot', bot)
span.end({ botId: bot.id, guildId: bot.guildId })
return { bot, token: typeof tokenInput === 'string' ? tokenInput : null }
} catch (err) {
this.log.error('bot.install error', {
guildId,
error: err?.message || String(err)
})
span.fail(err)
throw err
}
await this._ingestGuildBot(bot)
if (this.guild) this.guild.gossipBotInstall(bot)
this.emit('bot', bot)
return { bot, token: typeof tokenInput === 'string' ? tokenInput : null }
}
async verifyBotToken (tokenInput) {
@@ -9773,21 +9785,35 @@ class PearcordPlatform extends EventEmitter {
}
async sendBotMessage ({ botId, content, channelId } = {}) {
if (!this.guild?.guild) throw new Error('no guild')
const bot = await this.db.get(COLLECTIONS.GUILD_BOTS, { id: botId })
if (!bot) throw new Error('bot not found')
if (!botHasPermission(bot.permissions, BOT_PERMISSION.SEND_MESSAGES)) {
throw new Error('bot lacks SEND_MESSAGES permission')
}
const guildId = this.guild?.guild?.id || null
const chId = channelId || this.activeChannelId
if (!chId) throw new Error('no channel')
const prevChannel = this.activeChannelId
if (chId !== this.activeChannelId) await this.selectChannel(chId)
const msg = await this.messages.send(String(content ?? ''), { authorId: bot.id })
if (prevChannel && prevChannel !== chId) await this.selectChannel(prevChannel)
if (this.guild) this.guild.gossipMessage(msg)
await this._fanoutBotEvent(BOT_EVENTS.MESSAGE_CREATE, { message: msg })
return msg
const span = this.log.time('bot.message', { botId, guildId, channelId: chId })
try {
if (!this.guild?.guild) throw new Error('no guild')
const bot = await this.db.get(COLLECTIONS.GUILD_BOTS, { id: botId })
if (!bot) throw new Error('bot not found')
if (!botHasPermission(bot.permissions, BOT_PERMISSION.SEND_MESSAGES)) {
throw new Error('bot lacks SEND_MESSAGES permission')
}
if (!chId) throw new Error('no channel')
const prevChannel = this.activeChannelId
if (chId !== this.activeChannelId) await this.selectChannel(chId)
const msg = await this.messages.send(String(content ?? ''), { authorId: bot.id })
if (prevChannel && prevChannel !== chId) await this.selectChannel(prevChannel)
if (this.guild) this.guild.gossipMessage(msg)
await this._fanoutBotEvent(BOT_EVENTS.MESSAGE_CREATE, { message: msg })
span.end({ messageId: msg?.id, guildId, channelId: chId })
return msg
} catch (err) {
this.log.error('bot.message error', {
botId,
guildId,
channelId: chId,
error: err?.message || String(err)
})
span.fail(err)
throw err
}
}
async listBots () {
@@ -9957,7 +9983,13 @@ class PearcordPlatform extends EventEmitter {
}
async executeSlashCommand (content, opts = {}) {
const span = this.log.time('slash.invoke', { content: String(content || '').slice(0, 40) })
const guildId = this.mode === 'dm' ? DM_GUILD_ID : this.guild?.guild?.id || null
const channelId = this.activeChannelId
const span = this.log.time('slash.invoke', {
content: String(content || '').slice(0, 40),
guildId,
channelId
})
try {
if (!this.slashCommands || !this.messages) throw new Error('no channel')
const inv = parseSlashInvocation(content)
@@ -9993,11 +10025,13 @@ class PearcordPlatform extends EventEmitter {
}
const reply = `${header}\n${body}`
const msg = await this._sendPlainMessage(reply, opts)
span.end({ name: inv.name })
span.end({ name: inv.name, guildId, channelId })
return msg
} catch (err) {
this.log.error('slash.invoke error', {
content: String(content || '').slice(0, 40),
guildId,
channelId,
error: err?.message || String(err)
})
span.fail(err)