Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- **play command Sentry noise reduction**: `/play` now treats `DiscordAPIError[10062]` (`Unknown interaction`) as an interaction-expired path before logging command failures, and safely exits when `deferReply` already expired. This prevents recurring false-positive production error reports for already-handled interaction expiry events.

## [2.6.61] - 2026-04-03

### Fixed
Expand Down
105 changes: 97 additions & 8 deletions packages/bot/src/functions/music/commands/play/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const moveUserTrackToPriorityMock =
jest.fn<(queue: unknown, track: unknown) => void>()
const blendAutoplayTracksMock =
jest.fn<(queue: unknown, track: unknown) => Promise<void>>()
const interactionReplyMock = jest.fn<(payload: unknown) => Promise<void>>()

jest.mock('discord-player', () => ({
QueueRepeatMode: { OFF: 0, AUTOPLAY: 3 },
Expand Down Expand Up @@ -103,6 +104,10 @@ jest.mock('../../../../utils/music/collaborativePlaylist', () => ({
},
}))

jest.mock('../../../../utils/general/interactionReply', () => ({
interactionReply: (payload: unknown) => interactionReplyMock(payload),
}))

import playCommand from './index'

function createInteraction(guildId: string | null) {
Expand Down Expand Up @@ -185,8 +190,40 @@ describe('play command', () => {
} as any)

expect(interaction.deferReply).toHaveBeenCalled()
expect(interaction.editReply).toHaveBeenCalledWith(
expect.objectContaining({ embeds: expect.any(Array) }),
expect(interactionReplyMock).toHaveBeenCalledWith(
expect.objectContaining({
interaction,
content: expect.objectContaining({ embeds: expect.any(Array) }),
}),
)
expect(interaction.editReply).not.toHaveBeenCalled()
})

it('uses interactionReply for collaborative-limit replies', async () => {
const interaction = createInteraction('guild-1')
canAddTracksMock.mockReturnValue({
allowed: false,
limit: 1,
})

await playCommand.execute({
client: createClient(async () => ({})),
interaction,
} as any)

expect(errorLogMock).not.toHaveBeenCalled()
expect(interactionReplyMock).toHaveBeenCalledWith(
expect.objectContaining({
interaction,
content: expect.objectContaining({
embeds: expect.any(Array),
}),
}),
)
expect(debugLogMock).not.toHaveBeenCalledWith(
expect.objectContaining({
message: 'Play command interaction expired before editReply',
}),
)
})

Expand All @@ -212,7 +249,13 @@ describe('play command', () => {
'user-1',
1,
)
expect(interaction.editReply).toHaveBeenCalled()
expect(interactionReplyMock).toHaveBeenCalledWith(
expect.objectContaining({
interaction,
content: expect.objectContaining({ embeds: expect.any(Array) }),
}),
)
expect(interaction.editReply).not.toHaveBeenCalled()
expect(createSuccessEmbedMock).toHaveBeenCalled()
expect(client.player.play).toHaveBeenCalledWith(
expect.anything(),
Expand Down Expand Up @@ -473,16 +516,57 @@ describe('play command', () => {
data: expect.objectContaining({ guildId: 'guild-1' }),
}),
)
expect(interaction.editReply).toHaveBeenCalled()
expect(interactionReplyMock).toHaveBeenCalledWith(
expect.objectContaining({
interaction,
content: expect.objectContaining({
embeds: expect.any(Array),
}),
}),
)
expect(interaction.editReply).not.toHaveBeenCalled()
expect(createErrorEmbedMock).toHaveBeenCalledWith(
'Play Error',
expect.stringContaining('Could not find'),
)
})

it('logs a warning when sending the play error reply also fails', async () => {
it('ignores unknown interaction errors thrown during deferReply', async () => {
const interaction = createInteraction('guild-1')
interaction.deferReply.mockRejectedValue(
Object.assign(new Error('Unknown interaction'), { code: 10062 }),
)

await playCommand.execute({
client: createClient(async () => ({
track: { title: 'Song A', author: 'Artist A' },
searchResult: { playlist: null, tracks: [] },
})),
interaction,
} as any)

expect(errorLogMock).not.toHaveBeenCalled()
expect(interaction.editReply).not.toHaveBeenCalled()
})

it('ignores unknown interaction errors thrown during play flow', async () => {
const interaction = createInteraction('guild-1')

await playCommand.execute({
client: createClient(async () => {
throw Object.assign(new Error('Unknown interaction'), {
code: 10062,
})
}),
interaction,
} as any)

expect(errorLogMock).not.toHaveBeenCalled()
expect(interaction.editReply).not.toHaveBeenCalled()
})

it('uses interactionReply for play error replies', async () => {
const interaction = createInteraction('guild-1')
interaction.editReply.mockRejectedValue(new Error('reply failed'))

await playCommand.execute({
client: createClient(async () => {
Expand All @@ -491,10 +575,15 @@ describe('play command', () => {
interaction,
} as any)

expect(warnLogMock).toHaveBeenCalledWith(
expect(interactionReplyMock).toHaveBeenCalledWith(
expect.objectContaining({
interaction,
content: expect.objectContaining({ embeds: expect.any(Array) }),
}),
)
expect(warnLogMock).not.toHaveBeenCalledWith(
expect.objectContaining({
message: 'Failed to send play command error reply',
data: expect.objectContaining({ guildId: 'guild-1' }),
}),
)
})
Expand Down
67 changes: 48 additions & 19 deletions packages/bot/src/functions/music/commands/play/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { errorLog, debugLog, warnLog } from '@lucky/shared/utils'
import { guildSettingsService } from '@lucky/shared/services'
import { createErrorEmbed } from '../../../../utils/general/embeds'
import { createSuccessEmbed } from '../../../../utils/general/embeds'
import { interactionReply } from '../../../../utils/general/interactionReply'
import { collaborativePlaylistService } from '../../../../utils/music/collaborativePlaylist'
import { QueueRepeatMode } from 'discord-player'
import { resolveGuildQueue } from '../../../../utils/music/queueResolver'
Expand All @@ -19,6 +20,15 @@ import {

const DISCORD_UNKNOWN_INTERACTION_CODE = 10062

function isUnknownInteractionError(error: unknown): boolean {
return (
typeof error === 'object' &&
error !== null &&
'code' in error &&
(error as { code?: number }).code === DISCORD_UNKNOWN_INTERACTION_CODE
)
}

function isTrackAlreadyQueued(
queue: { tracks: { toArray?: () => Array<{ id?: string; url?: string }> } },
track: { id?: string; url?: string },
Expand Down Expand Up @@ -69,7 +79,12 @@ export default new Command({

const voiceChannel = member.voice.channel!

await interaction.deferReply()
try {
await interaction.deferReply()
} catch (error) {
if (isUnknownInteractionError(error)) return
throw error
}
Comment thread
LucasSantana-Dev marked this conversation as resolved.

const query = interaction.options.getString('query', true)
const collaborativeCheck = collaborativePlaylistService.canAddTracks(
Expand All @@ -78,13 +93,16 @@ export default new Command({
1,
)
if (!collaborativeCheck.allowed) {
await interaction.editReply({
embeds: [
createErrorEmbed(
'Contribution limit reached',
`Collaborative mode limit reached (${collaborativeCheck.limit} track requests per user).`,
),
],
await interactionReply({
interaction,
content: {
embeds: [
createErrorEmbed(
'Contribution limit reached',
`Collaborative mode limit reached (${collaborativeCheck.limit} track requests per user).`,
),
],
},
})
return
}
Expand Down Expand Up @@ -147,25 +165,36 @@ export default new Command({
1,
)

await interaction.editReply({ embeds: [embed] })
await interactionReply({
interaction,
content: { embeds: [embed] },
})
} catch (error) {
if (isUnknownInteractionError(error)) {
debugLog({
message: 'Play command interaction expired before reply',
data: { query, guildId: interaction.guildId },
})
return
}

errorLog({
message: 'Play command error:',
error,
data: { query, guildId: interaction.guildId },
})

const code = (error as { code?: number })?.code
if (code === DISCORD_UNKNOWN_INTERACTION_CODE) return

try {
await interaction.editReply({
embeds: [
createErrorEmbed(
'Play Error',
'Could not find or play the requested track',
),
],
await interactionReply({
interaction,
content: {
embeds: [
createErrorEmbed(
'Play Error',
'Could not find or play the requested track',
),
],
},
})
} catch (replyError) {
warnLog({
Expand Down