Skip to content

Commit

Permalink
Add Sentry Nuxt module (#5279)
Browse files Browse the repository at this point in the history
* Add Sentry Nuxt module

* Correctly call Sentry captureException in the app

* Fix unit tests

* Fix VR tests

* Add Sentry auth token for releases and sourcemaps

* Dotenv is already installed by Nuxt
  • Loading branch information
obulat authored Dec 17, 2024
1 parent b0ac47d commit 4555dcd
Show file tree
Hide file tree
Showing 20 changed files with 1,196 additions and 475 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci_cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ jobs:
outputs: type=docker,dest=/tmp/${{ matrix.image }}.tar
build-contexts: ${{ matrix.build-contexts }}
build-args: |
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SEMANTIC_VERSION=${{ needs.get-image-tag.outputs.image_tag }}
OV_PDM_VERSION=${{ steps.prepare-build-args.outputs.ov_pdm_version }}
CATALOG_PY_VERSION=${{ steps.prepare-build-args.outputs.catalog_py_version }}
Expand Down
3 changes: 3 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,6 @@ src/locales/scripts/wp-locales.json
# To prevent accidentally adding a hardcoded robots.txt, see
# /src/server-middleware/robots.js for the robots.txt file.
src/static/robots.txt

# Sentry Config File
.env.sentry-build-plugin
10 changes: 10 additions & 0 deletions frontend/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export default defineNuxtConfig({
"@nuxt/test-utils/module",
"@nuxtjs/sitemap",
"@nuxtjs/robots",
"@sentry/nuxt/module",
],
routeRules: {
"/photos/**": { redirect: { to: "/image/**", statusCode: 301 } },
Expand Down Expand Up @@ -126,4 +127,13 @@ export default defineNuxtConfig({
trailingSlash: false,
vueI18n: "./vue-i18n",
},
sentry: {
sourceMapsUploadOptions: {
org: "openverse",
project: "openverse-frontend",
},
},
sourcemap: {
client: "hidden",
},
})
3 changes: 1 addition & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,7 @@
"@nuxtjs/sitemap": "^7.0.0",
"@nuxtjs/tailwindcss": "^6.12.1",
"@pinia/nuxt": "^0.9.0",
"@sentry/node": "^8.26.0",
"@sentry/vue": "^8.26.0",
"@sentry/nuxt": "^8.45.0",
"@tailwindcss/typography": "^0.5.13",
"@vueuse/core": "^12.0.0",
"@wordpress/is-shallow-equal": "^5.3.0",
Expand Down
16 changes: 16 additions & 0 deletions frontend/sentry.client.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useAppConfig, useRuntimeConfig } from "#imports"

import * as Sentry from "@sentry/nuxt"

Sentry.init({
dsn: useRuntimeConfig().public.sentry.dsn,
environment: useRuntimeConfig().public.sentry.environment,
release: useAppConfig().semanticVersion,
ignoreErrors: [
// Can be safely ignored, @see https://github.com/WICG/resize-observer/issues/38
/ResizeObserver loop limit exceeded/i,
],

tracesSampleRate: 1.0,
})
Sentry.setContext("render context", { platform: "client" })
15 changes: 15 additions & 0 deletions frontend/sentry.server.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as Sentry from "@sentry/nuxt"
import dotenv from "dotenv"

// Necessary for loading environment variables before Nuxt is loaded
// @see the section on server setup: https://nuxt.com/modules/sentry
dotenv.config()

Sentry.init({
dsn: process.env.NUXT_PUBLIC_SENTRY_DSN,
environment: process.env.NUXT_PUBLIC_SENTRY_ENVIRONMENT,
release: process.env.SEMANTIC_VERSION,

tracesSampleRate: 1.0,
})
Sentry.setContext("render context", { platform: "server" })
37 changes: 0 additions & 37 deletions frontend/server/plugins/sentry.ts

This file was deleted.

6 changes: 3 additions & 3 deletions frontend/src/components/VSketchFabViewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ const emit = defineEmits<{ failure: [] }>()
const { t } = useI18n({ useScope: "global" })
const label = t("sketchfabIframeTitle", { sketchfab: "Sketchfab" })
const node = ref<Element | undefined>()
const { $sentry } = useNuxtApp()
const { $captureException, $captureMessage } = useNuxtApp()
const initSketchfab = async () => {
await loadScript(sketchfabUrl)
if (typeof window.Sketchfab === "undefined") {
$sentry.captureMessage("Unable to find window.Sketchfab after loading")
$captureMessage("Unable to find window.Sketchfab after loading")
return
}
Expand All @@ -44,7 +44,7 @@ const initSketchfab = async () => {
const sf = new window.Sketchfab(node.value)
sf.init(props.uid, {
error: (e: unknown) => {
$sentry.captureException(e)
$captureException(e)
emit("failure")
},
})
Expand Down
8 changes: 3 additions & 5 deletions frontend/src/plugins/01.api-token.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { defineNuxtPlugin, useRuntimeConfig } from "#imports"

import { Mutex, MutexInterface } from "async-mutex"
import axios from "axios"
import * as Sentry from "@sentry/nuxt"

import { debug, warn } from "~/utils/console"

import type { AxiosError } from "axios"
import type { NuxtApp } from "#app"

/* Process level state */

Expand Down Expand Up @@ -161,14 +161,12 @@ export const getApiAccessToken = async (): Promise<string | undefined> => {
return process.tokenData.accessToken
}

export default defineNuxtPlugin(async (app) => {
export default defineNuxtPlugin(async () => {
let openverseApiToken: string | undefined
try {
openverseApiToken = await getApiAccessToken()
} catch (e) {
const sentry =
app.ssrContext?.event.context.$sentry ?? (app as NuxtApp).$sentry
sentry.captureException(e)
Sentry.captureException(e)
}
return {
provide: {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/plugins/errors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { defineNuxtPlugin } from "#imports"

import { isAxiosError } from "axios"
import * as Sentry from "@sentry/nuxt"

import { ERR_UNKNOWN, ErrorCode, errorCodes } from "#shared/constants/errors"
import type { SupportedSearchType } from "#shared/constants/media"
Expand Down Expand Up @@ -122,8 +123,7 @@ export function recordError(
searchType: fetchingError.searchType,
})
} else {
const sentry = nuxtApp.ssrContext?.event.context.$sentry ?? nuxtApp.$sentry
sentry.captureException(originalError, { extra: { fetchingError } })
Sentry.captureException(originalError, { extra: { fetchingError } })
}
}

Expand Down
32 changes: 6 additions & 26 deletions frontend/src/plugins/sentry.client.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,13 @@
import { defineNuxtPlugin, useRuntimeConfig, useAppConfig } from "#imports"
import { defineNuxtPlugin } from "#imports"

import * as Sentry from "@sentry/vue"

export default defineNuxtPlugin((nuxtApp) => {
const {
public: { sentry },
} = useRuntimeConfig()

const { semanticVersion } = useAppConfig()

if (!sentry.dsn) {
console.warn("Sentry DSN wasn't provided")
}

Sentry.init({
dsn: sentry.dsn,
environment: sentry.environment,
release: semanticVersion,
app: nuxtApp.vueApp,
ignoreErrors: [
// Can be safely ignored, @see https://github.com/WICG/resize-observer/issues/38
/ResizeObserver loop limit exceeded/i,
],
})
Sentry.setContext("render context", { platform: "client" })
import * as Sentry from "@sentry/nuxt"

export default defineNuxtPlugin(() => {
const { captureException, captureMessage } = Sentry
return {
provide: {
sentry: Sentry,
captureException,
captureMessage,
},
}
})
7 changes: 4 additions & 3 deletions frontend/src/stores/active-media.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useNuxtApp } from "#imports"

import { defineStore } from "pinia"
import { useNuxtApp } from "#app"

import type { SupportedMediaType } from "#shared/constants/media"
import { audioErrorMessages } from "#shared/constants/audio"
Expand Down Expand Up @@ -85,8 +86,8 @@ export const useActiveMediaStore = defineStore(ACTIVE_MEDIA, {
? audioErrorMessages[err.name as keyof typeof audioErrorMessages]
: "err_unknown"
if (message === "err_unknown") {
const { $sentry } = useNuxtApp()
$sentry.captureException(err)
const { $captureException } = useNuxtApp()
$captureException(err)
}
this.setMessage({ message })
audio?.pause()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { expect } from "@playwright/test"
import { test } from "~~/test/playwright/utils/test"
import breakpoints from "~~/test/playwright/utils/breakpoints"
import {
Expand All @@ -6,7 +7,8 @@ import {
sleep,
} from "~~/test/playwright/utils/navigation"
import { setViewportToFullHeight } from "~~/test/playwright/utils/viewport"
import { languageDirections } from "~~/test/playwright/utils/i18n"
import { languageDirections, t } from "~~/test/playwright/utils/i18n"
import { getH1 } from "~~/test/playwright/utils/components"

import { ALL_MEDIA, supportedSearchTypes } from "#shared/constants/media"

Expand Down Expand Up @@ -56,6 +58,7 @@ breakpoints.describeXl(({ breakpoint, expectSnapshot }) => {
// eslint-disable-next-line playwright/no-networkidle
await page.waitForLoadState("networkidle")

await expect(getH1(page, t("404.title"))).toBeVisible()
await expectSnapshot(page, "generic-error-ltr", page, {
screenshotOptions: { fullPage: true },
})
Expand All @@ -77,6 +80,7 @@ for (const searchType of supportedSearchTypes) {
await preparePageForTests(page, breakpoint)
await goToSearchTerm(page, `SearchPage500error`, { searchType })

await expect(getH1(page, t("404.title"))).toBeVisible()
await expectSnapshot(page, "generic-error-ltr", page, {
screenshotOptions: { fullPage: true },
})
Expand All @@ -101,6 +105,7 @@ for (const searchType of supportedSearchTypes) {
searchType,
})

await expect(getH1(page, t("404.title"))).toBeVisible()
await expectSnapshot(page, "generic-error", page, {
dir,
screenshotOptions: {
Expand Down Expand Up @@ -136,6 +141,8 @@ for (const searchType of supportedSearchTypes) {
})
await goToSearchTerm(page, "cat", { dir, searchType, mode: "CSR" })

await expect(getH1(page, t("serverTimeout.heading", dir))).toBeVisible()

await setViewportToFullHeight(page)

await page.mouse.move(0, 82)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { expect } from "@playwright/test"
import { test } from "~~/test/playwright/utils/test"
import breakpoints from "~~/test/playwright/utils/breakpoints"
import {
isPageDesktop,
pathWithDir,
preparePageForTests,
} from "~~/test/playwright/utils/navigation"
Expand All @@ -11,6 +12,7 @@ import {
getHomepageSearchButton,
getLanguageSelect,
getLoadMoreButton,
getMenuButton,
} from "~~/test/playwright/utils/components"

test.describe.configure({ mode: "parallel" })
Expand All @@ -32,6 +34,11 @@ for (const contentPage of contentPages) {

await page.goto(pathWithDir(contentPage, dir))
// Ensure the page is hydrated
// eslint-disable-next-line playwright/no-conditional-in-test
if (!isPageDesktop(page)) {
// eslint-disable-next-line playwright/no-conditional-expect
await expect(getMenuButton(page, dir)).toBeEnabled()
}
await expect(page.locator("#language")).toHaveValue(
dir === "ltr" ? "en" : "ar"
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,6 @@ const stubs = {
RouterLink: RouterLinkStub,
}

const captureExceptionMock = vi.fn()

vi.mock("#app", async () => {
const original = await import("#app")
return {
...original,
useNuxtApp: vi.fn(() => ({
$sentry: {
captureException: captureExceptionMock,
},
})),
}
})

describe("AudioTrack", () => {
let options = null
let props = null
Expand Down Expand Up @@ -109,7 +95,6 @@ describe("AudioTrack", () => {
${"NotAllowedError"} | ${/Reproduction not allowed./i}
${"NotSupportedError"} | ${/This audio format is not supported by your browser./i}
${"AbortError"} | ${/You aborted playback./i}
${"UnknownError"} | ${/An unexpected error has occurred./i}
`(
"on play error displays a message instead of the waveform",
async ({ errorType, errorText }) => {
Expand Down Expand Up @@ -139,15 +124,6 @@ describe("AudioTrack", () => {
expect(playStub).toHaveBeenCalledTimes(1)
expect(pauseStub).toHaveBeenCalledTimes(1)
expect(getByText(errorText)).toBeVisible()

// Only the UnknownError should be sent to Sentry.
if (errorType === "UnknownError") {
// eslint-disable-next-line vitest/no-conditional-expect
expect(captureExceptionMock).toHaveBeenCalledWith(playError)
} else {
// eslint-disable-next-line vitest/no-conditional-expect
expect(captureExceptionMock).not.toHaveBeenCalled()
}
}
)

Expand Down
Loading

0 comments on commit 4555dcd

Please sign in to comment.