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
12 changes: 11 additions & 1 deletion sentry.client.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as Sentry from '@sentry/nuxt'
import { createSentryDataCollection, isLocalReportingHost, SENTRY_DSN } from './shared/sentry'
import { createSentryDataCollection, isClosedBroadcastChannelError, isLocalReportingHost, SENTRY_DSN } from './shared/sentry'

if (!import.meta.dev && window.location.protocol === 'https:' && !isLocalReportingHost(window.location.hostname)) {
// No `release` here on purpose. The Sentry bundler plugin injects the release
Expand All @@ -19,5 +19,15 @@ if (!import.meta.dev && window.location.protocol === 'https:' && !isLocalReporti
/Importing a module script failed/i,
/error loading dynamically imported module/i,
],
// nuxt-skew-protection's multi-tab plugin closes its BroadcastChannel on
// app:error, and an in-flight version update can still post to it. The
// rejection is a benign tab-close race (SKILLD-12), so drop that one
// signature and keep every other error.
beforeSend(event, hint) {
const values = event.exception?.values ?? []
if (isClosedBroadcastChannelError(hint.originalException) || values.some(isClosedBroadcastChannelError))
return null
return event
},
})
}
14 changes: 14 additions & 0 deletions shared/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,20 @@ export function isExpectedUpstreamOutageError(error: unknown): boolean {
return typeof message === 'string' && message.endsWith('unavailable upstream')
}

/**
* The message Chrome rejects a `postMessage` on a closed `BroadcastChannel`
* with (`InvalidStateError`, Sentry SKILLD-12). nuxt-skew-protection's
* multi-tab plugin closes its channel on `app:error`, and a version update
* already in flight can still post to it. The tab is erroring or closing
* anyway, and the other tabs pick the version up on their own poll, so the
* rejection is benign. The client Sentry config drops exactly that signature
* in `beforeSend`; a closed-channel post on any other target still lands.
*/
export function isClosedBroadcastChannelError(error: unknown): boolean {
const message = sentryExceptionMessage(error)
return message === 'Failed to execute \'postMessage\' on \'BroadcastChannel\': Channel is closed'
}

export function createSentryDataCollection() {
return {
userInfo: false,
Expand Down
33 changes: 32 additions & 1 deletion test/unit/sentry-cache-write-filter.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { isBestEffortCacheWriteError, isExpectedUpstreamOutageError } from '../../shared/sentry'
import { isBestEffortCacheWriteError, isClosedBroadcastChannelError, isExpectedUpstreamOutageError } from '../../shared/sentry'

describe('isClosedBroadcastChannelError', () => {
const closed = 'Failed to execute \'postMessage\' on \'BroadcastChannel\': Channel is closed'

it('matches the SKILLD-12 closed-channel postMessage the multi-tab plugin throws', () => {
expect(isClosedBroadcastChannelError(new DOMException(closed, 'InvalidStateError'))).toBe(true)
})

it('matches the Sentry-serialized exception, whose message arrives as value', () => {
expect(isClosedBroadcastChannelError({
type: 'InvalidStateError',
value: closed,
mechanism: { handled: false, type: 'auto.browser.global_handlers.onunhandledrejection' },
})).toBe(true)
})

it('ignores postMessage failures on other targets, which are not the tab-close race', () => {
expect(isClosedBroadcastChannelError(
new DOMException('Failed to execute \'postMessage\' on \'Worker\': Channel is closed', 'InvalidStateError'),
)).toBe(false)
expect(isClosedBroadcastChannelError(
new DOMException('Failed to execute \'postMessage\' on \'BroadcastChannel\': An object could not be cloned.', 'DataCloneError'),
)).toBe(false)
})

it('ignores generic invalid-state errors and values that are not errors', () => {
expect(isClosedBroadcastChannelError(new DOMException('The object is in an invalid state.', 'InvalidStateError'))).toBe(false)
expect(isClosedBroadcastChannelError(closed)).toBe(false)
expect(isClosedBroadcastChannelError(undefined)).toBe(false)
})
})

describe('isBestEffortCacheWriteError', () => {
it('matches the SKILLD-17 rate-limited KV write signature', () => {
Expand Down
Loading