diff --git a/.craft.yml b/.craft.yml index 1eb3f49530e7..d387e917307d 100644 --- a/.craft.yml +++ b/.craft.yml @@ -183,6 +183,8 @@ targets: format: base64 'npm:@sentry/bun': onlyIfPresent: /^sentry-bun-\d.*\.tgz$/ + 'npm:@sentry/cloudflare': + onlyIfPresent: /^sentry-cloudflare-\d.*\.tgz$/ 'npm:@sentry/deno': onlyIfPresent: /^sentry-deno-\d.*\.tgz$/ 'npm:@sentry/ember': diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9afeaec5947a..c160b8752a26 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -126,6 +126,7 @@ jobs: - *shared - *node - 'dev-packages/node-integration-tests/**' + - 'packages/nestjs/**' nextjs: - *shared - *browser @@ -408,10 +409,11 @@ jobs: DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} - name: Extract Profiling Node Prebuilt Binaries - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: - name: profiling-node-binaries-${{ github.sha }} + pattern: profiling-node-binaries-${{ github.sha }}-* path: ${{ github.workspace }}/packages/profiling-node/lib/ + merge-multiple: true - name: Pack tarballs run: yarn build:tarball @@ -901,16 +903,15 @@ jobs: run: yarn lerna run build:lib --scope @sentry/profiling-node - name: Extract Profiling Node Prebuilt Binaries - # @TODO: v4 breaks convenient merging of same name artifacts - # https://github.com/actions/upload-artifact/issues/478 if: | (needs.job_get_metadata.outputs.changed_profiling_node_bindings == 'true') || (needs.job_get_metadata.outputs.is_release == 'true') || (github.event_name != 'pull_request') - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: - name: profiling-node-binaries-${{ github.sha }} + pattern: profiling-node-binaries-${{ github.sha }}-* path: ${{ github.workspace }}/packages/profiling-node/lib/ + merge-multiple: true - name: Build Profiling tarball run: yarn build:tarball @@ -1230,11 +1231,11 @@ jobs: - name: Build Profiling Node run: yarn lerna run build:lib --scope @sentry/profiling-node - name: Extract Profiling Node Prebuilt Binaries - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: - name: profiling-node-binaries-${{ github.sha }} + pattern: profiling-node-binaries-${{ github.sha }}-* path: ${{ github.workspace }}/packages/profiling-node/lib/ - + merge-multiple: true - name: Restore tarball cache uses: actions/cache/restore@v4 with: @@ -1358,104 +1359,132 @@ jobs: # x64 glibc - os: ubuntu-20.04 node: 16 + binary: linux-x64-glibc-93 - os: ubuntu-20.04 node: 18 + binary: linux-x64-glibc-108 - os: ubuntu-20.04 node: 20 + binary: linux-x64-glibc-115 - os: ubuntu-20.04 node: 22 + binary: linux-x64-glibc-127 # x64 musl - os: ubuntu-20.04 container: node:16-alpine3.16 + binary: linux-x64-musl-93 node: 16 - os: ubuntu-20.04 container: node:18-alpine3.17 node: 18 + binary: linux-x64-musl-108 - os: ubuntu-20.04 container: node:20-alpine3.17 node: 20 + binary: linux-x64-musl-115 - os: ubuntu-20.04 container: node:22-alpine3.18 node: 22 + binary: linux-x64-musl-127 # arm64 glibc - os: ubuntu-20.04 arch: arm64 node: 16 + binary: linux-arm64-glibc-93 - os: ubuntu-20.04 arch: arm64 node: 18 + binary: linux-arm64-glibc-108 - os: ubuntu-20.04 arch: arm64 node: 20 + binary: linux-arm64-glibc-115 - os: ubuntu-20.04 arch: arm64 node: 22 + binary: linux-arm64-glibc-127 # arm64 musl - os: ubuntu-20.04 container: node:16-alpine3.16 arch: arm64 node: 16 + binary: linux-arm64-musl-93 - os: ubuntu-20.04 arch: arm64 container: node:18-alpine3.17 node: 18 + binary: linux-arm64-musl-108 - os: ubuntu-20.04 arch: arm64 container: node:20-alpine3.17 node: 20 + binary: linux-arm64-musl-115 - os: ubuntu-20.04 arch: arm64 container: node:22-alpine3.18 node: 22 + binary: linux-arm64-musl-127 # macos x64 - os: macos-13 node: 16 arch: x64 + binary: darwin-x64-93 - os: macos-13 node: 18 arch: x64 + binary: darwin-x64-108 - os: macos-13 node: 20 arch: x64 + binary: darwin-x64-115 - os: macos-13 node: 22 arch: x64 + binary: darwin-x64-127 # macos arm64 - os: macos-13 arch: arm64 node: 16 target_platform: darwin + binary: darwin-arm64-93 - os: macos-13 arch: arm64 node: 18 target_platform: darwin + binary: darwin-arm64-108 - os: macos-13 arch: arm64 node: 20 target_platform: darwin + binary: darwin-arm64-115 - os: macos-13 arch: arm64 node: 22 target_platform: darwin + binary: darwin-arm64-127 # windows x64 - os: windows-2022 node: 16 arch: x64 + binary: win32-x64-93 - os: windows-2022 node: 18 arch: x64 + binary: win32-x64-108 - os: windows-2022 node: 20 arch: x64 + binary: win32-x64-115 - os: windows-2022 node: 22 arch: x64 + binary: win32-x64-127 steps: - name: Setup (alpine) @@ -1587,10 +1616,8 @@ jobs: yarn lerna run test --scope @sentry/profiling-node - name: Archive Binary - # @TODO: v4 breaks convenient merging of same name artifacts - # https://github.com/actions/upload-artifact/issues/478 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: profiling-node-binaries-${{ github.sha }} - path: | - ${{ github.workspace }}/packages/profiling-node/lib/*.node + name: profiling-node-binaries-${{ github.sha }}-${{ matrix.binary }} + path: ${{ github.workspace }}/packages/profiling-node/lib/sentry_cpu_profiler-${{matrix.binary}}.node + if-no-files-found: error diff --git a/CHANGELOG.md b/CHANGELOG.md index 80a592cd4575..3eeb60765430 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ # Changelog -> [!IMPORTANT] Important -> + +> [!IMPORTANT] > If you are upgrading to the `8.x` versions of the SDK from `7.x` or below, make sure you follow our > [migration guide](https://docs.sentry.io/platforms/javascript/migration/) first. + ## Unreleased @@ -11,13 +12,39 @@ ## 8.23.0 -- feat(cloudflare): Add Cloudflare D1 instrumentation (#13142) +### Important Changes + +- **feat(cloudflare): Add Cloudflare D1 instrumentation (#13142)** + +This release includes support for Cloudflare D1, Cloudflare's serverless SQL database. To instrument your Cloudflare D1 +database, use the `instrumentD1WithSentry` method as follows: + +```ts +// env.DB is the D1 DB binding configured in your `wrangler.toml` +const db = instrumentD1WithSentry(env.DB); +// Now you can use the database as usual +await db.prepare('SELECT * FROM table WHERE id = ?').bind(1).run(); +``` + +### Other Changes + +- feat(cloudflare): Allow users to pass handler to sentryPagesPlugin (#13192) +- feat(cloudflare): Instrument scheduled handler (#13114) +- feat(core): Add `getTraceData` function (#13134) - feat(nestjs): Automatic instrumentation of nestjs interceptors before route execution (#13153) - feat(nestjs): Automatic instrumentation of nestjs pipes (#13137) - feat(nuxt): Filter out Nuxt build assets (#13148) -- feat(profiling): attach sdk info to chunks (#13145) +- feat(profiling): Attach sdk info to chunks (#13145) +- feat(solidstart): Add sentry `onBeforeResponse` middleware to enable distributed tracing (#13221) +- feat(solidstart): Filter out low quality transactions for build assets (#13222) - fix(browser): Avoid showing browser extension error message in non-`window` global scopes (#13156) +- fix(feedback): Call dialog.close() in dialog close callbacks in `\_loadAndRenderDialog` (#13203) - fix(nestjs): Inline Observable type to resolve missing 'rxjs' dependency (#13166) +- fix(nuxt): Detect pageload by adding flag in Vue router (#13171) +- fix(utils): Handle when requests get aborted in fetch instrumentation (#13202) +- ref(browser): Improve browserMetrics collection (#13062) + +Work in this release was contributed by @horochx. Thank you for your contribution! ## 8.22.0 diff --git a/README.md b/README.md index f164af08538a..3309f0521986 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ faster, so we can get back to enjoying technology. If you want to join us # Official Sentry SDKs for JavaScript + + Sentry for JavaScript + + This is the next line of Sentry JavaScript SDKs, comprised in the `@sentry/` namespace. It will provide a more convenient interface and improved consistency between various JavaScript environments. diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/init.js b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/init.js new file mode 100644 index 000000000000..7c200c542c56 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/init.js @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/subject.js b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/subject.js new file mode 100644 index 000000000000..78028b473ad7 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/subject.js @@ -0,0 +1,36 @@ +let controller; + +const startFetch = e => { + controller = new AbortController(); + const { signal } = controller; + + Sentry.startSpan( + { + name: 'with-abort-controller', + forceTransaction: true, + }, + async () => { + await fetch('http://localhost:7654/foo', { signal }) + .then(response => response.json()) + .then(data => { + console.log('Fetch succeeded:', data); + }) + .catch(err => { + if (err.name === 'AbortError') { + console.log('Fetch aborted'); + } else { + console.error('Fetch error:', err); + } + }); + }, + ); +}; + +const abortFetch = e => { + if (controller) { + controller.abort(); + } +}; + +document.querySelector('[data-test-id=start-button]').addEventListener('click', startFetch); +document.querySelector('[data-test-id=abort-button]').addEventListener('click', abortFetch); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/template.html b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/template.html new file mode 100644 index 000000000000..18cd917fe30f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/template.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/test.ts b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/test.ts new file mode 100644 index 000000000000..6cc3a0cd32a9 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/test.ts @@ -0,0 +1,41 @@ +import { expect } from '@playwright/test'; +import type { Event as SentryEvent } from '@sentry/types'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../../utils/helpers'; + +sentryTest('should handle aborted fetch calls', async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.route('**/foo', async () => { + // never fulfil this route because we abort the request as part of the test + }); + + const transactionEventPromise = getFirstSentryEnvelopeRequest(page); + + const hasAbortedFetchPromise = new Promise(resolve => { + page.on('console', msg => { + if (msg.type() === 'log' && msg.text() === 'Fetch aborted') { + resolve(); + } + }); + }); + + await page.goto(url); + + await page.locator('[data-test-id=start-button]').click(); + await page.locator('[data-test-id=abort-button]').click(); + + const transactionEvent = await transactionEventPromise; + + // assert that fetch calls do not return undefined + const fetchBreadcrumbs = transactionEvent.breadcrumbs?.filter( + ({ category, data }) => category === 'fetch' && data === undefined, + ); + expect(fetchBreadcrumbs).toHaveLength(0); + + await expect(hasAbortedFetchPromise).resolves.toBeUndefined(); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts index 2cfcbe58806e..f79505c6105a 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts @@ -25,7 +25,8 @@ sentryTest('should capture a LCP vital with element details.', async ({ browserN expect(eventData.measurements).toBeDefined(); expect(eventData.measurements?.lcp?.value).toBeDefined(); - expect(eventData.contexts?.trace?.data?.['lcp.element']).toBe('body > img'); - expect(eventData.contexts?.trace?.data?.['lcp.size']).toBe(107400); - expect(eventData.contexts?.trace?.data?.['lcp.url']).toBe('https://example.com/path/to/image.png'); + // XXX: This should be body > img, but it can be flakey as sometimes it will report + // the button as LCP. + expect(eventData.contexts?.trace?.data?.['lcp.element'].startsWith('body >')).toBe(true); + expect(eventData.contexts?.trace?.data?.['lcp.size']).toBeGreaterThan(0); }); diff --git a/dev-packages/browser-integration-tests/utils/staticAssets.ts b/dev-packages/browser-integration-tests/utils/staticAssets.ts index e293bd65237c..4b13159c58a4 100644 --- a/dev-packages/browser-integration-tests/utils/staticAssets.ts +++ b/dev-packages/browser-integration-tests/utils/staticAssets.ts @@ -27,7 +27,16 @@ export function addStaticAssetSymlink(localOutPath: string, originalPath: string // Only copy files once if (!fs.existsSync(newPath)) { - fs.symlinkSync(originalPath, newPath); + try { + fs.symlinkSync(originalPath, newPath); + } catch (error) { + // There must be some race condition here as some of our tests flakey + // because the file already exists. Let's catch and ignore + // only ignore these kind of errors + if (!`${error}`.includes('file already exists')) { + throw error; + } + } } symlinkAsset(newPath, path.join(localOutPath, fileName)); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/instrument.ts index f1f4de865435..4f16ebb36d11 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/instrument.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/instrument.ts @@ -5,4 +5,8 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + transportOptions: { + // We expect the app to send a lot of events in a short time + bufferSize: 1000, + }, }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nestjs-basic/start-event-proxy.mjs index e9917b9273da..a8ca8dcf1b3a 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/start-event-proxy.mjs @@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils'; startEventProxyServer({ port: 3031, - proxyServerName: 'nestjs', + proxyServerName: 'nestjs-basic', }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/cron-decorator.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/cron-decorator.test.ts index c13623337343..2c93e7c6adaa 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/cron-decorator.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/cron-decorator.test.ts @@ -2,11 +2,16 @@ import { expect, test } from '@playwright/test'; import { waitForEnvelopeItem } from '@sentry-internal/test-utils'; test('Cron job triggers send of in_progress envelope', async ({ baseURL }) => { - const inProgressEnvelopePromise = waitForEnvelopeItem('nestjs', envelope => { - return envelope[0].type === 'check_in'; + const inProgressEnvelopePromise = waitForEnvelopeItem('nestjs-basic', envelope => { + return envelope[0].type === 'check_in' && envelope[1]['status'] === 'in_progress'; + }); + + const okEnvelopePromise = waitForEnvelopeItem('nestjs-basic', envelope => { + return envelope[0].type === 'check_in' && envelope[1]['status'] === 'ok'; }); const inProgressEnvelope = await inProgressEnvelopePromise; + const okEnvelope = await okEnvelopePromise; expect(inProgressEnvelope[1]).toEqual( expect.objectContaining({ @@ -29,6 +34,22 @@ test('Cron job triggers send of in_progress envelope', async ({ baseURL }) => { }), ); + expect(okEnvelope[1]).toEqual( + expect.objectContaining({ + check_in_id: expect.any(String), + monitor_slug: 'test-cron-slug', + status: 'ok', + environment: 'qa', + duration: expect.any(Number), + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + }, + }, + }), + ); + // kill cron so tests don't get stuck await fetch(`${baseURL}/kill-test-cron`); }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/errors.test.ts index dad5d391bdde..cffc5f4946a3 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/errors.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('Sends exception to Sentry', async ({ baseURL }) => { - const errorEventPromise = waitForError('nestjs', event => { + const errorEventPromise = waitForError('nestjs-basic', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); @@ -32,7 +32,7 @@ test('Sends exception to Sentry', async ({ baseURL }) => { test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { let errorEventOccurred = false; - waitForError('nestjs', event => { + waitForError('nestjs-basic', event => { if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected 400 exception with id 123') { errorEventOccurred = true; } @@ -40,7 +40,7 @@ test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { return event?.transaction === 'GET /test-expected-400-exception/:id'; }); - waitForError('nestjs', event => { + waitForError('nestjs-basic', event => { if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected 500 exception with id 123') { errorEventOccurred = true; } @@ -48,11 +48,11 @@ test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { return event?.transaction === 'GET /test-expected-500-exception/:id'; }); - const transactionEventPromise400 = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise400 = waitForTransaction('nestjs-basic', transactionEvent => { return transactionEvent?.transaction === 'GET /test-expected-400-exception/:id'; }); - const transactionEventPromise500 = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise500 = waitForTransaction('nestjs-basic', transactionEvent => { return transactionEvent?.transaction === 'GET /test-expected-500-exception/:id'; }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/span-decorator.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/span-decorator.test.ts index 28c925727d89..4b3ea2c0ba40 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/span-decorator.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/span-decorator.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; test('Transaction includes span and correct value for decorated async function', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-span-decorator-async' @@ -37,7 +37,7 @@ test('Transaction includes span and correct value for decorated async function', }); test('Transaction includes span and correct value for decorated sync function', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-span-decorator-sync' diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts index 78b3e0d3102a..555b6357ade8 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; test('Sends an API route transaction', async ({ baseURL }) => { - const pageloadTransactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-transaction' @@ -125,7 +125,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { test('API route transaction includes nest middleware span. Spans created in and after middleware are nested correctly', async ({ baseURL, }) => { - const pageloadTransactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-middleware-instrumentation' @@ -205,7 +205,7 @@ test('API route transaction includes nest middleware span. Spans created in and test('API route transaction includes nest guard span and span started in guard is nested correctly', async ({ baseURL, }) => { - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-guard-instrumentation' @@ -268,10 +268,11 @@ test('API route transaction includes nest guard span and span started in guard i }); test('API route transaction includes nest pipe span for valid request', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' + transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' && + transactionEvent?.request?.url?.includes('/test-pipe-instrumentation/123') ); }); @@ -304,10 +305,11 @@ test('API route transaction includes nest pipe span for valid request', async ({ }); test('API route transaction includes nest pipe span for invalid request', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' + transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' && + transactionEvent?.request?.url?.includes('/test-pipe-instrumentation/abc') ); }); @@ -342,7 +344,7 @@ test('API route transaction includes nest pipe span for invalid request', async test('API route transaction includes nest interceptor span. Spans created in and after interceptor are nested correctly', async ({ baseURL, }) => { - const pageloadTransactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-interceptor-instrumentation' diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/instrument.ts index b5ca047e497c..1cf7b8ee1f76 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/instrument.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/instrument.ts @@ -6,4 +6,8 @@ Sentry.init({ tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], + transportOptions: { + // We expect the app to send a lot of events in a short time + bufferSize: 1000, + }, }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/start-event-proxy.mjs index e9917b9273da..5ba2a78c585c 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/start-event-proxy.mjs @@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils'; startEventProxyServer({ port: 3031, - proxyServerName: 'nestjs', + proxyServerName: 'nestjs-distributed-tracing', }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/propagation.test.ts index 2922435c542b..d928deac08fd 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/propagation.test.ts @@ -6,14 +6,14 @@ import { SpanJSON } from '@sentry/types'; test('Propagates trace for outgoing http requests', async ({ baseURL }) => { const id = crypto.randomUUID(); - const inboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-inbound-headers/${id}` ); }); - const outboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const outboundTransactionPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-http/${id}` @@ -66,7 +66,7 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { 'http.method': 'GET', 'http.scheme': 'http', 'http.target': `/test-outgoing-http/${id}`, - 'http.user_agent': 'node', + 'http.user_agent': expect.any(String), 'http.flavor': '1.1', 'net.transport': 'ip_tcp', 'net.host.ip': expect.any(String), @@ -121,14 +121,14 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { const id = crypto.randomUUID(); - const inboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-inbound-headers/${id}` ); }); - const outboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const outboundTransactionPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-fetch/${id}` @@ -181,7 +181,7 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { 'http.method': 'GET', 'http.scheme': 'http', 'http.target': `/test-outgoing-fetch/${id}`, - 'http.user_agent': 'node', + 'http.user_agent': expect.any(String), 'http.flavor': '1.1', 'net.transport': 'ip_tcp', 'net.host.ip': expect.any(String), @@ -234,7 +234,7 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { }); test('Propagates trace for outgoing external http requests', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-http-external-allowed` @@ -271,7 +271,7 @@ test('Propagates trace for outgoing external http requests', async ({ baseURL }) }); test('Does not propagate outgoing http requests not covered by tracePropagationTargets', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-http-external-disallowed` @@ -295,7 +295,7 @@ test('Does not propagate outgoing http requests not covered by tracePropagationT }); test('Propagates trace for outgoing external fetch requests', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-fetch-external-allowed` @@ -332,7 +332,7 @@ test('Propagates trace for outgoing external fetch requests', async ({ baseURL } }); test('Does not propagate outgoing fetch requests not covered by tracePropagationTargets', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-fetch-external-disallowed` diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/src/instrument.ts index f1f4de865435..4f16ebb36d11 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/src/instrument.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/src/instrument.ts @@ -5,4 +5,8 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + transportOptions: { + // We expect the app to send a lot of events in a short time + bufferSize: 1000, + }, }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/start-event-proxy.mjs index e9917b9273da..6ec54bc59e4f 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/start-event-proxy.mjs @@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils'; startEventProxyServer({ port: 3031, - proxyServerName: 'nestjs', + proxyServerName: 'nestjs-with-submodules', }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/errors.test.ts index 8d5885f146df..87b828dc8501 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/errors.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('Sends unexpected exception to Sentry if thrown in module with global filter', async ({ baseURL }) => { - const errorEventPromise = waitForError('nestjs', event => { + const errorEventPromise = waitForError('nestjs-with-submodules', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an uncaught exception!'; }); @@ -32,7 +32,7 @@ test('Sends unexpected exception to Sentry if thrown in module with global filte test('Sends unexpected exception to Sentry if thrown in module that was registered before Sentry', async ({ baseURL, }) => { - const errorEventPromise = waitForError('nestjs', event => { + const errorEventPromise = waitForError('nestjs-with-submodules', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an uncaught exception!'; }); @@ -64,7 +64,7 @@ test('Does not send exception to Sentry if user-defined global exception filter }) => { let errorEventOccurred = false; - waitForError('nestjs', event => { + waitForError('nestjs-with-submodules', event => { if (!event.type && event.exception?.values?.[0]?.value === 'Something went wrong in the example module!') { errorEventOccurred = true; } @@ -72,7 +72,7 @@ test('Does not send exception to Sentry if user-defined global exception filter return event?.transaction === 'GET /example-module/expected-exception'; }); - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('nestjs-with-submodules', transactionEvent => { return transactionEvent?.transaction === 'GET /example-module/expected-exception'; }); @@ -91,7 +91,7 @@ test('Does not send exception to Sentry if user-defined local exception filter a }) => { let errorEventOccurred = false; - waitForError('nestjs', event => { + waitForError('nestjs-with-submodules', event => { if ( !event.type && event.exception?.values?.[0]?.value === 'Something went wrong in the example module with local filter!' @@ -102,7 +102,7 @@ test('Does not send exception to Sentry if user-defined local exception filter a return event?.transaction === 'GET /example-module-local-filter/expected-exception'; }); - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('nestjs-with-submodules', transactionEvent => { return transactionEvent?.transaction === 'GET /example-module-local-filter/expected-exception'; }); @@ -119,7 +119,7 @@ test('Does not send exception to Sentry if user-defined local exception filter a test('Does not handle expected exception if exception is thrown in module registered before Sentry', async ({ baseURL, }) => { - const errorEventPromise = waitForError('nestjs', event => { + const errorEventPromise = waitForError('nestjs-with-submodules', event => { return !event.type && event.exception?.values?.[0]?.value === 'Something went wrong in the example module!'; }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/transactions.test.ts index 25375f5fd962..887284585ae1 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/transactions.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; test('Sends an API route transaction from module', async ({ baseURL }) => { - const pageloadTransactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-with-submodules', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /example-module/transaction' diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/instrument.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/instrument.ts index f1f4de865435..4f16ebb36d11 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/instrument.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/instrument.ts @@ -5,4 +5,8 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + transportOptions: { + // We expect the app to send a lot of events in a short time + bufferSize: 1000, + }, }); diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/start-event-proxy.mjs index e9917b9273da..a521d4f7d4fc 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/start-event-proxy.mjs @@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils'; startEventProxyServer({ port: 3031, - proxyServerName: 'nestjs', + proxyServerName: 'node-nestjs-basic', }); diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/cron-decorator.test.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/cron-decorator.test.ts index c13623337343..1475a1449f44 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/cron-decorator.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/cron-decorator.test.ts @@ -2,11 +2,16 @@ import { expect, test } from '@playwright/test'; import { waitForEnvelopeItem } from '@sentry-internal/test-utils'; test('Cron job triggers send of in_progress envelope', async ({ baseURL }) => { - const inProgressEnvelopePromise = waitForEnvelopeItem('nestjs', envelope => { - return envelope[0].type === 'check_in'; + const inProgressEnvelopePromise = waitForEnvelopeItem('node-nestjs-basic', envelope => { + return envelope[0].type === 'check_in' && envelope[1]['status'] === 'in_progress'; + }); + + const okEnvelopePromise = waitForEnvelopeItem('node-nestjs-basic', envelope => { + return envelope[0].type === 'check_in' && envelope[1]['status'] === 'ok'; }); const inProgressEnvelope = await inProgressEnvelopePromise; + const okEnvelope = await okEnvelopePromise; expect(inProgressEnvelope[1]).toEqual( expect.objectContaining({ @@ -29,6 +34,22 @@ test('Cron job triggers send of in_progress envelope', async ({ baseURL }) => { }), ); + expect(okEnvelope[1]).toEqual( + expect.objectContaining({ + check_in_id: expect.any(String), + monitor_slug: 'test-cron-slug', + status: 'ok', + environment: 'qa', + duration: expect.any(Number), + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + }, + }, + }), + ); + // kill cron so tests don't get stuck await fetch(`${baseURL}/kill-test-cron`); }); diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/errors.test.ts index dad5d391bdde..11eafc38f430 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/errors.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('Sends exception to Sentry', async ({ baseURL }) => { - const errorEventPromise = waitForError('nestjs', event => { + const errorEventPromise = waitForError('node-nestjs-basic', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); @@ -32,7 +32,7 @@ test('Sends exception to Sentry', async ({ baseURL }) => { test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { let errorEventOccurred = false; - waitForError('nestjs', event => { + waitForError('node-nestjs-basic', event => { if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected 400 exception with id 123') { errorEventOccurred = true; } @@ -40,7 +40,7 @@ test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { return event?.transaction === 'GET /test-expected-400-exception/:id'; }); - waitForError('nestjs', event => { + waitForError('node-nestjs-basic', event => { if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected 500 exception with id 123') { errorEventOccurred = true; } @@ -48,11 +48,11 @@ test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { return event?.transaction === 'GET /test-expected-500-exception/:id'; }); - const transactionEventPromise400 = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise400 = waitForTransaction('node-nestjs-basic', transactionEvent => { return transactionEvent?.transaction === 'GET /test-expected-400-exception/:id'; }); - const transactionEventPromise500 = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise500 = waitForTransaction('node-nestjs-basic', transactionEvent => { return transactionEvent?.transaction === 'GET /test-expected-500-exception/:id'; }); diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/span-decorator.test.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/span-decorator.test.ts index 28c925727d89..831dfd4400dc 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/span-decorator.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/span-decorator.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; test('Transaction includes span and correct value for decorated async function', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('node-nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-span-decorator-async' @@ -37,7 +37,7 @@ test('Transaction includes span and correct value for decorated async function', }); test('Transaction includes span and correct value for decorated sync function', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('node-nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-span-decorator-sync' diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts index 62c882eb7f4b..cb04bc06839e 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; test('Sends an API route transaction', async ({ baseURL }) => { - const pageloadTransactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const pageloadTransactionEventPromise = waitForTransaction('node-nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-transaction' @@ -125,7 +125,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { test('API route transaction includes nest middleware span. Spans created in and after middleware are nested correctly', async ({ baseURL, }) => { - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('node-nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-middleware-instrumentation' @@ -205,7 +205,7 @@ test('API route transaction includes nest middleware span. Spans created in and test('API route transaction includes nest guard span and span started in guard is nested correctly', async ({ baseURL, }) => { - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('node-nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-guard-instrumentation' @@ -268,7 +268,7 @@ test('API route transaction includes nest guard span and span started in guard i }); test('API route transaction includes nest pipe span for valid request', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('node-nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' @@ -304,7 +304,7 @@ test('API route transaction includes nest pipe span for valid request', async ({ }); test('API route transaction includes nest pipe span for invalid request', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('node-nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' @@ -342,7 +342,7 @@ test('API route transaction includes nest pipe span for invalid request', async test('API route transaction includes nest interceptor span. Spans created in and after interceptor are nested correctly', async ({ baseURL, }) => { - const pageloadTransactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const pageloadTransactionEventPromise = waitForTransaction('node-nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-interceptor-instrumentation' diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/src/instrument.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/src/instrument.ts index b5ca047e497c..1cf7b8ee1f76 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/src/instrument.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/src/instrument.ts @@ -6,4 +6,8 @@ Sentry.init({ tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], + transportOptions: { + // We expect the app to send a lot of events in a short time + bufferSize: 1000, + }, }); diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/start-event-proxy.mjs index e9917b9273da..1db7d30f8680 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/start-event-proxy.mjs @@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils'; startEventProxyServer({ port: 3031, - proxyServerName: 'nestjs', + proxyServerName: 'node-nestjs-distributed-tracing', }); diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/tests/propagation.test.ts index 2922435c542b..49b827ca7e27 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/tests/propagation.test.ts @@ -6,14 +6,14 @@ import { SpanJSON } from '@sentry/types'; test('Propagates trace for outgoing http requests', async ({ baseURL }) => { const id = crypto.randomUUID(); - const inboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('node-nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-inbound-headers/${id}` ); }); - const outboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const outboundTransactionPromise = waitForTransaction('node-nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-http/${id}` @@ -121,14 +121,14 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { const id = crypto.randomUUID(); - const inboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('node-nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-inbound-headers/${id}` ); }); - const outboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const outboundTransactionPromise = waitForTransaction('node-nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-fetch/${id}` @@ -234,7 +234,7 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { }); test('Propagates trace for outgoing external http requests', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('node-nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-http-external-allowed` @@ -271,7 +271,7 @@ test('Propagates trace for outgoing external http requests', async ({ baseURL }) }); test('Does not propagate outgoing http requests not covered by tracePropagationTargets', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('node-nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-http-external-disallowed` @@ -295,7 +295,7 @@ test('Does not propagate outgoing http requests not covered by tracePropagationT }); test('Propagates trace for outgoing external fetch requests', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('node-nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-fetch-external-allowed` @@ -332,7 +332,7 @@ test('Propagates trace for outgoing external fetch requests', async ({ baseURL } }); test('Does not propagate outgoing fetch requests not covered by tracePropagationTargets', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('node-nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-fetch-external-disallowed` diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/playwright.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/playwright.config.ts index f270a5ad9b48..d1094993131d 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/playwright.config.ts @@ -8,6 +8,9 @@ const nuxtConfigOptions: ConfigOptions = { }, }; +/* Make sure to import from '@nuxt/test-utils/playwright' in the tests + * Like this: import { expect, test } from '@nuxt/test-utils/playwright' */ + const config = getPlaywrightConfig({ startCommand: `pnpm preview`, use: { ...nuxtConfigOptions }, diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/performance.client.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/performance.client.test.ts new file mode 100644 index 000000000000..66c8c9dfce2d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/performance.client.test.ts @@ -0,0 +1,31 @@ +import { expect, test } from '@nuxt/test-utils/playwright'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('sends a pageload root span with a parameterized URL', async ({ page }) => { + const transactionPromise = waitForTransaction('nuxt-3', async transactionEvent => { + return transactionEvent.transaction === '/test-param/:param()'; + }); + + await page.goto(`/test-param/1234`); + + const rootSpan = await transactionPromise; + + expect(rootSpan).toMatchObject({ + contexts: { + trace: { + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.pageload.vue', + 'sentry.op': 'pageload', + 'params.param': '1234', + }, + op: 'pageload', + origin: 'auto.pageload.vue', + }, + }, + transaction: '/test-param/:param()', + transaction_info: { + source: 'route', + }, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/src/pages/SSE.tsx b/dev-packages/e2e-tests/test-applications/react-router-6/src/pages/SSE.tsx index 49e53b09cfa2..64a9f5717114 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6/src/pages/SSE.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-6/src/pages/SSE.tsx @@ -2,17 +2,24 @@ import * as Sentry from '@sentry/react'; // biome-ignore lint/nursery/noUnusedImports: Need React import for JSX import * as React from 'react'; -const fetchSSE = async ({ timeout }: { timeout: boolean }) => { +const fetchSSE = async ({ timeout, abort = false }: { timeout: boolean; abort?: boolean }) => { Sentry.startSpanManual({ name: 'sse stream using fetch' }, async span => { + const controller = new AbortController(); + const res = await Sentry.startSpan({ name: 'sse fetch call' }, async () => { const endpoint = `http://localhost:8080/${timeout ? 'sse-timeout' : 'sse'}`; - return await fetch(endpoint); + + const signal = controller.signal; + return await fetch(endpoint, { signal }); }); const stream = res.body; const reader = stream?.getReader(); const readChunk = async () => { + if (abort) { + controller.abort(); + } const readRes = await reader?.read(); if (readRes?.done) { return; @@ -42,6 +49,9 @@ const SSE = () => { + ); }; diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts b/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts index 5d4533726e36..92c06543c0b8 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts @@ -34,6 +34,43 @@ test('Waits for sse streaming when creating spans', async ({ page }) => { expect(resolveBodyDuration).toBe(2); }); +test('Waits for sse streaming when sse has been explicitly aborted', async ({ page }) => { + await page.goto('/sse'); + + const transactionPromise = waitForTransaction('react-router-6', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const fetchButton = page.locator('id=fetch-sse-abort'); + await fetchButton.click(); + + const rootSpan = await transactionPromise; + console.log(JSON.stringify(rootSpan, null, 2)); + const sseFetchCall = rootSpan.spans?.filter(span => span.description === 'sse fetch call')[0] as SpanJSON; + const httpGet = rootSpan.spans?.filter(span => span.description === 'GET http://localhost:8080/sse')[0] as SpanJSON; + + expect(sseFetchCall).toBeDefined(); + expect(httpGet).toBeDefined(); + + expect(sseFetchCall?.timestamp).toBeDefined(); + expect(sseFetchCall?.start_timestamp).toBeDefined(); + expect(httpGet?.timestamp).toBeDefined(); + expect(httpGet?.start_timestamp).toBeDefined(); + + // http headers get sent instantly from the server + const resolveDuration = Math.round((sseFetchCall.timestamp as number) - sseFetchCall.start_timestamp); + + // body streams after 0s because it has been aborted + const resolveBodyDuration = Math.round((httpGet.timestamp as number) - httpGet.start_timestamp); + + expect(resolveDuration).toBe(0); + expect(resolveBodyDuration).toBe(0); + + // validate abort eror was thrown by inspecting console + const consoleBreadcrumb = rootSpan.breadcrumbs?.find(breadcrumb => breadcrumb.category === 'console'); + expect(consoleBreadcrumb?.message).toBe('Could not fetch sse AbortError: BodyStreamBuffer was aborted'); +}); + test('Aborts when stream takes longer than 5s', async ({ page }) => { await page.goto('/sse'); diff --git a/dev-packages/node-integration-tests/suites/express/without-tracing/test.ts b/dev-packages/node-integration-tests/suites/express/without-tracing/test.ts index 236c978dcd9a..7c304062bc22 100644 --- a/dev-packages/node-integration-tests/suites/express/without-tracing/test.ts +++ b/dev-packages/node-integration-tests/suites/express/without-tracing/test.ts @@ -23,24 +23,7 @@ test('correctly applies isolation scope even without tracing', done => { }, }, }) - .expect({ - event: { - transaction: 'GET /test/isolationScope/2', - tags: { - global: 'tag', - 'isolation-scope': 'tag', - 'isolation-scope-2': '2', - }, - // Request is correctly set - request: { - url: expect.stringContaining('/test/isolationScope/2'), - headers: { - 'user-agent': expect.stringContaining(''), - }, - }, - }, - }) .start(done); - runner.makeRequest('get', '/test/isolationScope/1').then(() => runner.makeRequest('get', '/test/isolationScope/2')); + runner.makeRequest('get', '/test/isolationScope/1'); }); diff --git a/dev-packages/test-utils/src/event-proxy-server.ts b/dev-packages/test-utils/src/event-proxy-server.ts index 01395202d990..58c39de95c8c 100644 --- a/dev-packages/test-utils/src/event-proxy-server.ts +++ b/dev-packages/test-utils/src/event-proxy-server.ts @@ -90,7 +90,7 @@ export async function startProxyServer( const callback: OnRequest = onRequest || (async (eventCallbackListeners, proxyRequest, proxyRequestBody, eventBuffer) => { - eventBuffer.push({ data: proxyRequestBody, timestamp: Date.now() }); + eventBuffer.push({ data: proxyRequestBody, timestamp: getNanosecondTimestamp() }); eventCallbackListeners.forEach(listener => { listener(proxyRequestBody); @@ -234,7 +234,7 @@ export async function startEventProxyServer(options: EventProxyServerOptions): P const dataString = Buffer.from(JSON.stringify(data)).toString('base64'); - eventBuffer.push({ data: dataString, timestamp: Date.now() }); + eventBuffer.push({ data: dataString, timestamp: getNanosecondTimestamp() }); eventCallbackListeners.forEach(listener => { listener(dataString); @@ -259,7 +259,7 @@ export async function waitForPlainRequest( return new Promise((resolve, reject) => { const request = http.request( - `http://localhost:${eventCallbackServerPort}/?timestamp=${Date.now()}`, + `http://localhost:${eventCallbackServerPort}/?timestamp=${getNanosecondTimestamp()}`, {}, response => { let eventContents = ''; @@ -289,7 +289,7 @@ export async function waitForPlainRequest( export async function waitForRequest( proxyServerName: string, callback: (eventData: SentryRequestCallbackData) => Promise | boolean, - timestamp: number = Date.now(), + timestamp: number = getNanosecondTimestamp(), ): Promise { const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName); @@ -345,7 +345,7 @@ export async function waitForRequest( export function waitForEnvelopeItem( proxyServerName: string, callback: (envelopeItem: EnvelopeItem) => Promise | boolean, - timestamp: number = Date.now(), + timestamp: number = getNanosecondTimestamp(), ): Promise { return new Promise((resolve, reject) => { waitForRequest( @@ -370,7 +370,7 @@ export function waitForError( proxyServerName: string, callback: (errorEvent: Event) => Promise | boolean, ): Promise { - const timestamp = Date.now(); + const timestamp = getNanosecondTimestamp(); return new Promise((resolve, reject) => { waitForEnvelopeItem( proxyServerName, @@ -392,7 +392,7 @@ export function waitForSession( proxyServerName: string, callback: (session: SerializedSession) => Promise | boolean, ): Promise { - const timestamp = Date.now(); + const timestamp = getNanosecondTimestamp(); return new Promise((resolve, reject) => { waitForEnvelopeItem( proxyServerName, @@ -414,7 +414,7 @@ export function waitForTransaction( proxyServerName: string, callback: (transactionEvent: Event) => Promise | boolean, ): Promise { - const timestamp = Date.now(); + const timestamp = getNanosecondTimestamp(); return new Promise((resolve, reject) => { waitForEnvelopeItem( proxyServerName, @@ -448,3 +448,12 @@ async function retrieveCallbackServerPort(serverName: string): Promise { throw e; } } + +/** + * We do nanosecond checking because the waitFor* calls and the fetch requests may come very shortly after one another. + */ +function getNanosecondTimestamp(): number { + const NS_PER_SEC = 1e9; + const [seconds, nanoseconds] = process.hrtime(); + return seconds * NS_PER_SEC + nanoseconds; +} diff --git a/package.json b/package.json index 604d67e9ef5b..7c737d5a10d6 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "test:pr": "nx affected -t test --exclude \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\"", "test:pr:browser": "yarn test:pr --exclude \"@sentry/{core,utils,opentelemetry,bun,deno,node,profiling-node,aws-serverless,google-cloud-serverless,nextjs,nestjs,astro,cloudflare,solidstart,nuxt,remix,gatsby,sveltekit,vercel-edge}\"", "test:pr:node": "ts-node ./scripts/node-unit-tests.ts --affected", - "test:ci:browser": "lerna run test --ignore \"@sentry/{core,utils,opentelemetry,bun,deno,node,profiling-node,aws-serverless,google-cloud-serverless,nextjs,nestjs,astro,cloudflare,solidstart,nuxt,remix,gatsby,sveltekit,vercel-edge}\"", + "test:ci:browser": "lerna run test --ignore \"@sentry/{core,utils,opentelemetry,bun,deno,node,profiling-node,aws-serverless,google-cloud-serverless,nextjs,nestjs,astro,cloudflare,solidstart,nuxt,remix,gatsby,sveltekit,vercel-edge}\" --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\"", "test:ci:node": "ts-node ./scripts/node-unit-tests.ts", "test:ci:bun": "lerna run test --scope @sentry/bun", "yalc:publish": "lerna run yalc:publish" diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index a235b6a16b83..1084643584d6 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -55,6 +55,7 @@ export { getSentryRelease, getSpanDescendants, getSpanStatusFromHttpCode, + getTraceData, graphqlIntegration, hapiIntegration, httpIntegration, diff --git a/packages/astro/src/server/meta.ts b/packages/astro/src/server/meta.ts deleted file mode 100644 index 42d50c9d865d..000000000000 --- a/packages/astro/src/server/meta.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { - getDynamicSamplingContextFromClient, - getDynamicSamplingContextFromSpan, - getRootSpan, - spanToTraceHeader, -} from '@sentry/core'; -import type { Client, Scope, Span } from '@sentry/types'; -import { - TRACEPARENT_REGEXP, - dynamicSamplingContextToSentryBaggageHeader, - generateSentryTraceHeader, - logger, -} from '@sentry/utils'; - -/** - * Extracts the tracing data from the current span or from the client's scope - * (via transaction or propagation context) and renders the data to tags. - * - * This function creates two serialized tags: - * - `` - * - `` - * - * TODO: Extract this later on and export it from the Core or Node SDK - * - * @param span the currently active span - * @param client the SDK's client - * - * @returns an object with the two serialized tags - */ -export function getTracingMetaTags( - span: Span | undefined, - scope: Scope, - client: Client | undefined, -): { sentryTrace: string; baggage?: string } { - const { dsc, sampled, traceId } = scope.getPropagationContext(); - const rootSpan = span && getRootSpan(span); - - const sentryTrace = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, undefined, sampled); - - const dynamicSamplingContext = rootSpan - ? getDynamicSamplingContextFromSpan(rootSpan) - : dsc - ? dsc - : client - ? getDynamicSamplingContextFromClient(traceId, client) - : undefined; - - const baggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); - - const isValidSentryTraceHeader = TRACEPARENT_REGEXP.test(sentryTrace); - if (!isValidSentryTraceHeader) { - logger.warn('Invalid sentry-trace data. Returning empty tag'); - } - - const validBaggage = isValidBaggageString(baggage); - if (!validBaggage) { - logger.warn('Invalid baggage data. Returning empty tag'); - } - - return { - sentryTrace: ``, - baggage: baggage && ``, - }; -} - -/** - * Tests string against baggage spec as defined in: - * - * - W3C Baggage grammar: https://www.w3.org/TR/baggage/#definition - * - RFC7230 token definition: https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6 - * - * exported for testing - */ -export function isValidBaggageString(baggage?: string): boolean { - if (!baggage || !baggage.length) { - return false; - } - const keyRegex = "[-!#$%&'*+.^_`|~A-Za-z0-9]+"; - const valueRegex = '[!#-+-./0-9:<=>?@A-Z\\[\\]a-z{-}]+'; - const spaces = '\\s*'; - // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- RegExp for readability, no user input - const baggageRegex = new RegExp( - `^${keyRegex}${spaces}=${spaces}${valueRegex}(${spaces},${spaces}${keyRegex}${spaces}=${spaces}${valueRegex})*$`, - ); - return baggageRegex.test(baggage); -} diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index adfac32843f8..6b668f462489 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -14,7 +14,7 @@ import type { Client, Scope, Span, SpanAttributes } from '@sentry/types'; import { addNonEnumerableProperty, objectify, stripUrlQueryAndFragment } from '@sentry/utils'; import type { APIContext, MiddlewareResponseHandler } from 'astro'; -import { getTracingMetaTags } from './meta'; +import { getTraceData } from '@sentry/node'; type MiddlewareOptions = { /** @@ -189,9 +189,17 @@ function addMetaTagToHead(htmlChunk: string, scope: Scope, client: Client, span? if (typeof htmlChunk !== 'string') { return htmlChunk; } + const { 'sentry-trace': sentryTrace, baggage } = getTraceData(span, scope, client); + + if (!sentryTrace) { + return htmlChunk; + } + + const sentryTraceMeta = ``; + const baggageMeta = baggage && ``; + + const content = `\n${sentryTraceMeta}`.concat(baggageMeta ? `\n${baggageMeta}` : '', '\n'); - const { sentryTrace, baggage } = getTracingMetaTags(span, scope, client); - const content = `\n${sentryTrace}\n${baggage}\n`; return htmlChunk.replace('', content); } diff --git a/packages/astro/test/integration/middleware/index.test.ts b/packages/astro/test/integration/middleware/index.test.ts index 3b12508feaa7..3c48086a2ee2 100644 --- a/packages/astro/test/integration/middleware/index.test.ts +++ b/packages/astro/test/integration/middleware/index.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it, vi } from 'vitest'; import { onRequest } from '../../../src/integration/middleware'; vi.mock('../../../src/server/meta', () => ({ - getTracingMetaTags: () => ({ + getTracingMetaTagValues: () => ({ sentryTrace: '', baggage: '', }), diff --git a/packages/astro/test/server/middleware.test.ts b/packages/astro/test/server/middleware.test.ts index a678fcceaee6..58405c8d1c12 100644 --- a/packages/astro/test/server/middleware.test.ts +++ b/packages/astro/test/server/middleware.test.ts @@ -1,12 +1,13 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import * as SentryCore from '@sentry/core'; import * as SentryNode from '@sentry/node'; import type { Client, Span } from '@sentry/types'; -import { vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { handleRequest, interpolateRouteFromUrlAndParams } from '../../src/server/middleware'; vi.mock('../../src/server/meta', () => ({ - getTracingMetaTags: () => ({ + getTracingMetaTagValues: () => ({ sentryTrace: '', baggage: '', }), @@ -28,10 +29,18 @@ describe('sentryMiddleware', () => { setPropagationContext: vi.fn(), getSpan: getSpanMock, setSDKProcessingMetadata: setSDKProcessingMetadataMock, + getPropagationContext: () => ({}), } as any; }); vi.spyOn(SentryNode, 'getActiveSpan').mockImplementation(getSpanMock); vi.spyOn(SentryNode, 'getClient').mockImplementation(() => ({}) as Client); + vi.spyOn(SentryNode, 'getTraceData').mockImplementation(() => ({ + 'sentry-trace': '123', + baggage: 'abc', + })); + vi.spyOn(SentryCore, 'getDynamicSamplingContextFromSpan').mockImplementation(() => ({ + transaction: 'test', + })); }); const nextResult = Promise.resolve(new Response(null, { status: 200, headers: new Headers() })); diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index eee24075bdf8..95b2d553f2d4 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -20,6 +20,7 @@ export { getCurrentScope, getGlobalScope, getIsolationScope, + getTraceData, setCurrentClient, Scope, SDK_VERSION, diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index 02c044322bd3..43ea45dd4a08 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -99,10 +99,10 @@ export function startTrackingWebVitals(): () => void { */ export function startTrackingLongTasks(): void { addPerformanceInstrumentationHandler('longtask', ({ entries }) => { + if (!getActiveSpan()) { + return; + } for (const entry of entries) { - if (!getActiveSpan()) { - return; - } const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime); const duration = msToSec(entry.duration); @@ -129,12 +129,12 @@ export function startTrackingLongAnimationFrames(): void { // we directly observe `long-animation-frame` events instead of through the web-vitals // `observe` helper function. const observer = new PerformanceObserver(list => { + if (!getActiveSpan()) { + return; + } for (const entry of list.getEntries() as PerformanceLongAnimationFrameTiming[]) { - if (!getActiveSpan()) { - return; - } if (!entry.scripts[0]) { - return; + continue; } const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime); @@ -143,20 +143,19 @@ export function startTrackingLongAnimationFrames(): void { const attributes: SpanAttributes = { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', }; + const initialScript = entry.scripts[0]; - if (initialScript) { - const { invoker, invokerType, sourceURL, sourceFunctionName, sourceCharPosition } = initialScript; - attributes['browser.script.invoker'] = invoker; - attributes['browser.script.invoker_type'] = invokerType; - if (sourceURL) { - attributes['code.filepath'] = sourceURL; - } - if (sourceFunctionName) { - attributes['code.function'] = sourceFunctionName; - } - if (sourceCharPosition !== -1) { - attributes['browser.script.source_char_position'] = sourceCharPosition; - } + const { invoker, invokerType, sourceURL, sourceFunctionName, sourceCharPosition } = initialScript; + attributes['browser.script.invoker'] = invoker; + attributes['browser.script.invoker_type'] = invokerType; + if (sourceURL) { + attributes['code.filepath'] = sourceURL; + } + if (sourceFunctionName) { + attributes['code.function'] = sourceFunctionName; + } + if (sourceCharPosition !== -1) { + attributes['browser.script.source_char_position'] = sourceCharPosition; } const span = startInactiveSpan({ @@ -179,11 +178,10 @@ export function startTrackingLongAnimationFrames(): void { */ export function startTrackingInteractions(): void { addPerformanceInstrumentationHandler('event', ({ entries }) => { + if (!getActiveSpan()) { + return; + } for (const entry of entries) { - if (!getActiveSpan()) { - return; - } - if (entry.name === 'click') { const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime); const duration = msToSec(entry.duration); diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 199013b959ff..287dbc26eeee 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -40,6 +40,7 @@ export { getCurrentScope, getGlobalScope, getIsolationScope, + getTraceData, setCurrentClient, Scope, SDK_VERSION, diff --git a/packages/cloudflare/README.md b/packages/cloudflare/README.md index 7c7512e2ed1d..f7de52a56e88 100644 --- a/packages/cloudflare/README.md +++ b/packages/cloudflare/README.md @@ -15,7 +15,7 @@ - [Official SDK Docs](https://docs.sentry.io/quickstart/) - [TypeDoc](http://getsentry.github.io/sentry-javascript/) -**Note: This SDK is unreleased. Please follow the +**Note: This SDK is in an alpha state. Please follow the [tracking GH issue](https://github.com/getsentry/sentry-javascript/issues/12620) for updates.** ## Install @@ -72,6 +72,37 @@ export const onRequest = [ ]; ``` +If you need to access the `context` object (for example to grab environmental variables), you can pass a function to +`sentryPagesPlugin` that takes the `context` object as an argument and returns `init` options: + +```javascript +export const onRequest = Sentry.sentryPagesPlugin(context => ({ + dsn: context.env.SENTRY_DSN, + tracesSampleRate: 1.0, +})); +``` + +If you do not have access to the `onRequest` middleware API, you can use the `wrapRequestHandler` API instead. + +Here is an example with SvelteKit: + +```javascript +// hooks.server.js +import * as Sentry from '@sentry/cloudflare'; + +export const handle = ({ event, resolve }) => { + const requestHandlerOptions = { + options: { + dsn: event.platform.env.SENTRY_DSN, + tracesSampleRate: 1.0, + }, + request: event.request, + context: event.platform.ctx, + }; + return Sentry.wrapRequestHandler(requestHandlerOptions, () => resolve(event)); +}; +``` + ## Setup (Cloudflare Workers) To use this SDK, wrap your handler with the `withSentry` function. This will initialize the SDK and hook into the @@ -143,8 +174,50 @@ You can use the `instrumentD1WithSentry` method to instrument [Cloudflare D1](ht Cloudflare's serverless SQL database with Sentry. ```javascript +import * as Sentry from '@sentry/cloudflare'; + // env.DB is the D1 DB binding configured in your `wrangler.toml` -const db = instrumentD1WithSentry(env.DB); +const db = Sentry.instrumentD1WithSentry(env.DB); // Now you can use the database as usual await db.prepare('SELECT * FROM table WHERE id = ?').bind(1).run(); ``` + +## Cron Monitoring (Cloudflare Workers) + +[Sentry Crons](https://docs.sentry.io/product/crons/) allows you to monitor the uptime and performance of any scheduled, +recurring job in your application. + +To instrument your cron triggers, use the `Sentry.withMonitor` API in your +[`Scheduled` handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/). + +```js +export default { + async scheduled(event, env, ctx) { + ctx.waitUntil( + Sentry.withMonitor('your-cron-name', () => { + return doSomeTaskOnASchedule(); + }), + ); + }, +}; +``` + +You can also use supply a monitor config to upsert cron monitors with additional metadata: + +```js +const monitorConfig = { + schedule: { + type: 'crontab', + value: '* * * * *', + }, + checkinMargin: 2, // In minutes. Optional. + maxRuntime: 10, // In minutes. Optional. + timezone: 'America/Los_Angeles', // Optional. +}; + +export default { + async scheduled(event, env, ctx) { + Sentry.withMonitor('your-cron-name', () => doSomeTaskOnASchedule(), monitorConfig); + }, +}; +``` diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index 2b77932537f1..c80dfa758efe 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -47,10 +47,9 @@ "@cloudflare/workers-types": "^4.x" }, "devDependencies": { - "@cloudflare/workers-types": "^4.20240722.0", + "@cloudflare/workers-types": "^4.20240725.0", "@types/node": "^14.18.0", - "miniflare": "^3.20240718.0", - "wrangler": "^3.65.1" + "wrangler": "^3.67.1" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/cloudflare/src/handler.ts b/packages/cloudflare/src/handler.ts index 65f3cf8bcbf1..51260f01d755 100644 --- a/packages/cloudflare/src/handler.ts +++ b/packages/cloudflare/src/handler.ts @@ -1,7 +1,21 @@ -import type { ExportedHandler, ExportedHandlerFetchHandler } from '@cloudflare/workers-types'; -import type { Options } from '@sentry/types'; +import type { + ExportedHandler, + ExportedHandlerFetchHandler, + ExportedHandlerScheduledHandler, +} from '@cloudflare/workers-types'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + captureException, + flush, + startSpan, + withIsolationScope, +} from '@sentry/core'; import { setAsyncLocalStorageAsyncContextStrategy } from './async'; +import type { CloudflareOptions } from './client'; import { wrapRequestHandler } from './request'; +import { addCloudResourceContext } from './scope-utils'; +import { init } from './sdk'; /** * Extract environment generic from exported handler. @@ -21,7 +35,7 @@ type ExtractEnv

= P extends ExportedHandler ? Env : never; */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function withSentry>( - optionsCallback: (env: ExtractEnv) => Options, + optionsCallback: (env: ExtractEnv) => CloudflareOptions, handler: E, ): E { setAsyncLocalStorageAsyncContextStrategy(); @@ -40,5 +54,52 @@ export function withSentry>( (handler.fetch as any).__SENTRY_INSTRUMENTED__ = true; } + if ( + 'scheduled' in handler && + typeof handler.scheduled === 'function' && + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any + !(handler.scheduled as any).__SENTRY_INSTRUMENTED__ + ) { + handler.scheduled = new Proxy(handler.scheduled, { + apply(target, thisArg, args: Parameters>>) { + const [event, env, context] = args; + return withIsolationScope(isolationScope => { + const options = optionsCallback(env); + const client = init(options); + isolationScope.setClient(client); + + addCloudResourceContext(isolationScope); + + return startSpan( + { + op: 'faas.cron', + name: `Scheduled Cron ${event.cron}`, + attributes: { + 'faas.cron': event.cron, + 'faas.time': new Date(event.scheduledTime).toISOString(), + 'faas.trigger': 'timer', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task', + }, + }, + async () => { + try { + return await (target.apply(thisArg, args) as ReturnType); + } catch (e) { + captureException(e, { mechanism: { handled: false, type: 'cloudflare' } }); + throw e; + } finally { + context.waitUntil(flush(2000)); + } + }, + ); + }); + }, + }); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any + (handler.scheduled as any).__SENTRY_INSTRUMENTED__ = true; + } + return handler; } diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index a4a466fa5bb5..2f77f96f4e33 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -55,6 +55,7 @@ export { setMeasurement, getActiveSpan, getRootSpan, + getTraceData, startSpan, startInactiveSpan, startSpanManual, @@ -87,6 +88,8 @@ export { export { withSentry } from './handler'; export { sentryPagesPlugin } from './pages-plugin'; +export { wrapRequestHandler } from './request'; + export { CloudflareClient } from './client'; export { getDefaultIntegrations } from './sdk'; diff --git a/packages/cloudflare/src/pages-plugin.ts b/packages/cloudflare/src/pages-plugin.ts index f2c46efd86f2..8bdc806b5693 100644 --- a/packages/cloudflare/src/pages-plugin.ts +++ b/packages/cloudflare/src/pages-plugin.ts @@ -7,23 +7,48 @@ import { wrapRequestHandler } from './request'; * * Initializes the SDK and wraps cloudflare pages requests with SDK instrumentation. * - * @example + * @example Simple usage + * * ```javascript * // functions/_middleware.js * import * as Sentry from '@sentry/cloudflare'; * * export const onRequest = Sentry.sentryPagesPlugin({ - * dsn: process.env.SENTRY_DSN, - * tracesSampleRate: 1.0, + * dsn: process.env.SENTRY_DSN, + * tracesSampleRate: 1.0, * }); * ``` + * + * @example Usage with handler function to access context for environmental variables + * + * ```javascript + * import * as Sentry from '@sentry/cloudflare'; + * + * const const onRequest = Sentry.sentryPagesPlugin((context) => ({ + * dsn: context.env.SENTRY_DSN, + * tracesSampleRate: 1.0, + * }) + * ``` + * + * @param handlerOrOptions Configuration options or a function that returns configuration options. + * @returns A plugin function that can be used in Cloudflare Pages. */ export function sentryPagesPlugin< Env = unknown, // eslint-disable-next-line @typescript-eslint/no-explicit-any Params extends string = any, Data extends Record = Record, ->(options: CloudflareOptions): PagesPluginFunction { + // Although it is not ideal to use `any` here, it makes usage more flexible for different setups. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + PluginParams = any, +>( + handlerOrOptions: + | CloudflareOptions + | ((context: EventPluginContext) => CloudflareOptions), +): PagesPluginFunction { setAsyncLocalStorageAsyncContextStrategy(); - return context => wrapRequestHandler({ options, request: context.request, context }, () => context.next()); + return context => { + const options = typeof handlerOrOptions === 'function' ? handlerOrOptions(context) : handlerOrOptions; + return wrapRequestHandler({ options, request: context.request, context }, () => context.next()); + }; } diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts index b10037ec8bc0..560c17afb9e7 100644 --- a/packages/cloudflare/src/request.ts +++ b/packages/cloudflare/src/request.ts @@ -11,9 +11,10 @@ import { startSpan, withIsolationScope, } from '@sentry/core'; -import type { Scope, SpanAttributes } from '@sentry/types'; -import { stripUrlQueryAndFragment, winterCGRequestToRequestData } from '@sentry/utils'; +import type { SpanAttributes } from '@sentry/types'; +import { stripUrlQueryAndFragment } from '@sentry/utils'; import type { CloudflareOptions } from './client'; +import { addCloudResourceContext, addCultureContext, addRequest } from './scope-utils'; import { init } from './sdk'; interface RequestHandlerWrapperOptions { @@ -96,28 +97,3 @@ export function wrapRequestHandler( ); }); } - -/** - * Set cloud resource context on scope. - */ -function addCloudResourceContext(scope: Scope): void { - scope.setContext('cloud_resource', { - 'cloud.provider': 'cloudflare', - }); -} - -/** - * Set culture context on scope - */ -function addCultureContext(scope: Scope, cf: IncomingRequestCfProperties): void { - scope.setContext('culture', { - timezone: cf.timezone, - }); -} - -/** - * Set request data on scope - */ -function addRequest(scope: Scope, request: Request): void { - scope.setSDKProcessingMetadata({ request: winterCGRequestToRequestData(request) }); -} diff --git a/packages/cloudflare/src/scope-utils.ts b/packages/cloudflare/src/scope-utils.ts new file mode 100644 index 000000000000..1f5bbce8f0fc --- /dev/null +++ b/packages/cloudflare/src/scope-utils.ts @@ -0,0 +1,29 @@ +import type { IncomingRequestCfProperties } from '@cloudflare/workers-types'; + +import type { Scope } from '@sentry/types'; +import { winterCGRequestToRequestData } from '@sentry/utils'; + +/** + * Set cloud resource context on scope. + */ +export function addCloudResourceContext(scope: Scope): void { + scope.setContext('cloud_resource', { + 'cloud.provider': 'cloudflare', + }); +} + +/** + * Set culture context on scope + */ +export function addCultureContext(scope: Scope, cf: IncomingRequestCfProperties): void { + scope.setContext('culture', { + timezone: cf.timezone, + }); +} + +/** + * Set request data on scope + */ +export function addRequest(scope: Scope, request: Request): void { + scope.setSDKProcessingMetadata({ request: winterCGRequestToRequestData(request) }); +} diff --git a/packages/cloudflare/src/sdk.ts b/packages/cloudflare/src/sdk.ts index ca2035388c12..a16a9e578a06 100644 --- a/packages/cloudflare/src/sdk.ts +++ b/packages/cloudflare/src/sdk.ts @@ -7,9 +7,9 @@ import { linkedErrorsIntegration, requestDataIntegration, } from '@sentry/core'; -import type { Integration, Options } from '@sentry/types'; +import type { Integration } from '@sentry/types'; import { stackParserFromStackParserOptions } from '@sentry/utils'; -import type { CloudflareClientOptions } from './client'; +import type { CloudflareClientOptions, CloudflareOptions } from './client'; import { CloudflareClient } from './client'; import { fetchIntegration } from './integrations/fetch'; @@ -17,7 +17,7 @@ import { makeCloudflareTransport } from './transport'; import { defaultStackParser } from './vendor/stacktrace'; /** Get the default integrations for the Cloudflare SDK. */ -export function getDefaultIntegrations(options: Options): Integration[] { +export function getDefaultIntegrations(options: CloudflareOptions): Integration[] { const sendDefaultPii = options.sendDefaultPii ?? false; return [ dedupeIntegration(), @@ -32,7 +32,7 @@ export function getDefaultIntegrations(options: Options): Integration[] { /** * Initializes the cloudflare SDK. */ -export function init(options: Options): CloudflareClient | undefined { +export function init(options: CloudflareOptions): CloudflareClient | undefined { if (options.defaultIntegrations === undefined) { options.defaultIntegrations = getDefaultIntegrations(options); } diff --git a/packages/cloudflare/test/handler.test.ts b/packages/cloudflare/test/handler.test.ts index 238fbd987c90..861360c7906f 100644 --- a/packages/cloudflare/test/handler.test.ts +++ b/packages/cloudflare/test/handler.test.ts @@ -3,49 +3,221 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; +import type { ScheduledController } from '@cloudflare/workers-types'; +import * as SentryCore from '@sentry/core'; +import type { Event } from '@sentry/types'; +import { CloudflareClient } from '../src/client'; import { withSentry } from '../src/handler'; const MOCK_ENV = { SENTRY_DSN: 'https://public@dsn.ingest.sentry.io/1337', }; -describe('sentryPagesPlugin', () => { +describe('withSentry', () => { beforeEach(() => { vi.clearAllMocks(); }); - test('gets env from handler', async () => { - const handler = { - fetch(_request, _env, _context) { - return new Response('test'); - }, - } satisfies ExportedHandler; + describe('fetch handler', () => { + test('executes options callback with env', async () => { + const handler = { + fetch(_request, _env, _context) { + return new Response('test'); + }, + } satisfies ExportedHandler; - const optionsCallback = vi.fn().mockReturnValue({}); + const optionsCallback = vi.fn().mockReturnValue({}); - const wrappedHandler = withSentry(optionsCallback, handler); - await wrappedHandler.fetch(new Request('https://example.com'), MOCK_ENV, createMockExecutionContext()); + const wrappedHandler = withSentry(optionsCallback, handler); + await wrappedHandler.fetch(new Request('https://example.com'), MOCK_ENV, createMockExecutionContext()); - expect(optionsCallback).toHaveBeenCalledTimes(1); - expect(optionsCallback).toHaveBeenLastCalledWith(MOCK_ENV); + expect(optionsCallback).toHaveBeenCalledTimes(1); + expect(optionsCallback).toHaveBeenLastCalledWith(MOCK_ENV); + }); + + test('passes through the handler response', async () => { + const response = new Response('test'); + const handler = { + async fetch(_request, _env, _context) { + return response; + }, + } satisfies ExportedHandler; + + const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler); + const result = await wrappedHandler.fetch( + new Request('https://example.com'), + MOCK_ENV, + createMockExecutionContext(), + ); + + expect(result).toBe(response); + }); }); - test('passes through the response from the handler', async () => { - const response = new Response('test'); - const handler = { - async fetch(_request, _env, _context) { - return response; - }, - } satisfies ExportedHandler; - - const wrappedHandler = withSentry(() => ({}), handler); - const result = await wrappedHandler.fetch( - new Request('https://example.com'), - MOCK_ENV, - createMockExecutionContext(), - ); - - expect(result).toBe(response); + describe('scheduled handler', () => { + test('executes options callback with env', async () => { + const handler = { + scheduled(_controller, _env, _context) { + return; + }, + } satisfies ExportedHandler; + + const optionsCallback = vi.fn().mockReturnValue({}); + + const wrappedHandler = withSentry(optionsCallback, handler); + await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext()); + + expect(optionsCallback).toHaveBeenCalledTimes(1); + expect(optionsCallback).toHaveBeenLastCalledWith(MOCK_ENV); + }); + + test('flushes the event after the handler is done using the cloudflare context.waitUntil', async () => { + const handler = { + scheduled(_controller, _env, _context) { + return; + }, + } satisfies ExportedHandler; + + const context = createMockExecutionContext(); + const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler); + await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, context); + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(context.waitUntil).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(context.waitUntil).toHaveBeenLastCalledWith(expect.any(Promise)); + }); + + test('creates a cloudflare client and sets it on the handler', async () => { + const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind'); + const handler = { + scheduled(_controller, _env, _context) { + return; + }, + } satisfies ExportedHandler; + + const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler); + await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext()); + + expect(initAndBindSpy).toHaveBeenCalledTimes(1); + expect(initAndBindSpy).toHaveBeenLastCalledWith(CloudflareClient, expect.any(Object)); + }); + + describe('scope instrumentation', () => { + test('adds cloud resource context', async () => { + const handler = { + scheduled(_controller, _env, _context) { + SentryCore.captureMessage('cloud_resource'); + return; + }, + } satisfies ExportedHandler; + + let sentryEvent: Event = {}; + const wrappedHandler = withSentry( + env => ({ + dsn: env.SENTRY_DSN, + beforeSend(event) { + sentryEvent = event; + return null; + }, + }), + handler, + ); + await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext()); + + expect(sentryEvent.contexts?.cloud_resource).toEqual({ 'cloud.provider': 'cloudflare' }); + }); + }); + + describe('error instrumentation', () => { + test('captures errors thrown by the handler', async () => { + const captureExceptionSpy = vi.spyOn(SentryCore, 'captureException'); + const error = new Error('test'); + + expect(captureExceptionSpy).not.toHaveBeenCalled(); + + const handler = { + scheduled(_controller, _env, _context) { + throw error; + }, + } satisfies ExportedHandler; + + const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler); + try { + await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext()); + } catch { + // ignore + } + + expect(captureExceptionSpy).toHaveBeenCalledTimes(1); + expect(captureExceptionSpy).toHaveBeenLastCalledWith(error, { + mechanism: { handled: false, type: 'cloudflare' }, + }); + }); + + test('re-throws the error after capturing', async () => { + const error = new Error('test'); + const handler = { + scheduled(_controller, _env, _context) { + throw error; + }, + } satisfies ExportedHandler; + + const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler); + + let thrownError: Error | undefined; + try { + await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext()); + } catch (e: any) { + thrownError = e; + } + + expect(thrownError).toBe(error); + }); + }); + + describe('tracing instrumentation', () => { + test('creates a span that wraps scheduled invocation', async () => { + const handler = { + scheduled(_controller, _env, _context) { + return; + }, + } satisfies ExportedHandler; + + let sentryEvent: Event = {}; + const wrappedHandler = withSentry( + env => ({ + dsn: env.SENTRY_DSN, + tracesSampleRate: 1, + beforeSendTransaction(event) { + sentryEvent = event; + return null; + }, + }), + handler, + ); + + await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext()); + + expect(sentryEvent.transaction).toEqual('Scheduled Cron 0 0 0 * * *'); + expect(sentryEvent.spans).toHaveLength(0); + expect(sentryEvent.contexts?.trace).toEqual({ + data: { + 'sentry.origin': 'auto.faas.cloudflare', + 'sentry.op': 'faas.cron', + 'faas.cron': '0 0 0 * * *', + 'faas.time': expect.any(String), + 'faas.trigger': 'timer', + 'sentry.sample_rate': 1, + 'sentry.source': 'task', + }, + op: 'faas.cron', + origin: 'auto.faas.cloudflare', + span_id: expect.any(String), + trace_id: expect.any(String), + }); + }); + }); }); }); @@ -55,3 +227,11 @@ function createMockExecutionContext(): ExecutionContext { passThroughOnException: vi.fn(), }; } + +function createMockScheduledController(): ScheduledController { + return { + scheduledTime: 123, + cron: '0 0 0 * * *', + noRetry: vi.fn(), + }; +} diff --git a/packages/cloudflare/test/pages-plugin.test.ts b/packages/cloudflare/test/pages-plugin.test.ts index 6e8b87351f8e..b1781dc397af 100644 --- a/packages/cloudflare/test/pages-plugin.test.ts +++ b/packages/cloudflare/test/pages-plugin.test.ts @@ -15,6 +15,28 @@ describe('sentryPagesPlugin', () => { vi.clearAllMocks(); }); + test('calls handler function if a function is provided', async () => { + const mockOptionsHandler = vi.fn().mockReturnValue(MOCK_OPTIONS); + const mockOnRequest = sentryPagesPlugin(mockOptionsHandler); + + const MOCK_CONTEXT = { + request: new Request('https://example.com'), + functionPath: 'test', + waitUntil: vi.fn(), + passThroughOnException: vi.fn(), + next: () => Promise.resolve(new Response('test')), + env: { ASSETS: { fetch: vi.fn() } }, + params: {}, + data: {}, + pluginArgs: MOCK_OPTIONS, + }; + + await mockOnRequest(MOCK_CONTEXT); + + expect(mockOptionsHandler).toHaveBeenCalledTimes(1); + expect(mockOptionsHandler).toHaveBeenLastCalledWith(MOCK_CONTEXT); + }); + test('passes through the response from the handler', async () => { const response = new Response('test'); const mockOnRequest = sentryPagesPlugin(MOCK_OPTIONS); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1971bb8c94bd..5c21c8e484ed 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -82,6 +82,7 @@ export { } from './utils/spanUtils'; export { parseSampleRate } from './utils/parseSampleRate'; export { applySdkMetadata } from './utils/sdkMetadata'; +export { getTraceData } from './utils/traceData'; export { DEFAULT_ENVIRONMENT } from './constants'; export { addBreadcrumb } from './breadcrumbs'; export { functionToStringIntegration } from './integrations/functiontostring'; diff --git a/packages/core/src/utils/traceData.ts b/packages/core/src/utils/traceData.ts new file mode 100644 index 000000000000..abc05f449365 --- /dev/null +++ b/packages/core/src/utils/traceData.ts @@ -0,0 +1,89 @@ +import type { Client, Scope, Span } from '@sentry/types'; +import { + TRACEPARENT_REGEXP, + dynamicSamplingContextToSentryBaggageHeader, + generateSentryTraceHeader, + logger, +} from '@sentry/utils'; +import { getClient, getCurrentScope } from '../currentScopes'; +import { getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan } from '../tracing'; +import { getActiveSpan, getRootSpan, spanToTraceHeader } from './spanUtils'; + +type TraceData = { + 'sentry-trace'?: string; + baggage?: string; +}; + +/** + * Extracts trace propagation data from the current span or from the client's scope (via transaction or propagation + * context) and serializes it to `sentry-trace` and `baggage` values to strings. These values can be used to propagate + * a trace via our tracing Http headers or Html `` tags. + * + * This function also applies some validation to the generated sentry-trace and baggage values to ensure that + * only valid strings are returned. + * + * @param span a span to take the trace data from. By default, the currently active span is used. + * @param scope the scope to take trace data from By default, the active current scope is used. + * @param client the SDK's client to take trace data from. By default, the current client is used. + * + * @returns an object with the tracing data values. The object keys are the name of the tracing key to be used as header + * or meta tag name. + */ +export function getTraceData(span?: Span, scope?: Scope, client?: Client): TraceData { + const clientToUse = client || getClient(); + const scopeToUse = scope || getCurrentScope(); + const spanToUse = span || getActiveSpan(); + + const { dsc, sampled, traceId } = scopeToUse.getPropagationContext(); + const rootSpan = spanToUse && getRootSpan(spanToUse); + + const sentryTrace = spanToUse ? spanToTraceHeader(spanToUse) : generateSentryTraceHeader(traceId, undefined, sampled); + + const dynamicSamplingContext = rootSpan + ? getDynamicSamplingContextFromSpan(rootSpan) + : dsc + ? dsc + : clientToUse + ? getDynamicSamplingContextFromClient(traceId, clientToUse) + : undefined; + + const baggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); + + const isValidSentryTraceHeader = TRACEPARENT_REGEXP.test(sentryTrace); + if (!isValidSentryTraceHeader) { + logger.warn('Invalid sentry-trace data. Cannot generate trace data'); + return {}; + } + + const validBaggage = isValidBaggageString(baggage); + if (!validBaggage) { + logger.warn('Invalid baggage data. Not returning "baggage" value'); + } + + return { + 'sentry-trace': sentryTrace, + ...(validBaggage && { baggage }), + }; +} + +/** + * Tests string against baggage spec as defined in: + * + * - W3C Baggage grammar: https://www.w3.org/TR/baggage/#definition + * - RFC7230 token definition: https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6 + * + * exported for testing + */ +export function isValidBaggageString(baggage?: string): boolean { + if (!baggage || !baggage.length) { + return false; + } + const keyRegex = "[-!#$%&'*+.^_`|~A-Za-z0-9]+"; + const valueRegex = '[!#-+-./0-9:<=>?@A-Z\\[\\]a-z{-}]+'; + const spaces = '\\s*'; + // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- RegExp for readability, no user input + const baggageRegex = new RegExp( + `^${keyRegex}${spaces}=${spaces}${valueRegex}(${spaces},${spaces}${keyRegex}${spaces}=${spaces}${valueRegex})*$`, + ); + return baggageRegex.test(baggage); +} diff --git a/packages/astro/test/server/meta.test.ts b/packages/core/test/lib/utils/traceData.test.ts similarity index 65% rename from packages/astro/test/server/meta.test.ts rename to packages/core/test/lib/utils/traceData.test.ts index 8b65beaa4eaf..e757926ca30d 100644 --- a/packages/astro/test/server/meta.test.ts +++ b/packages/core/test/lib/utils/traceData.test.ts @@ -1,9 +1,7 @@ -import * as SentryCore from '@sentry/core'; -import { SentrySpan } from '@sentry/core'; -import type { Transaction } from '@sentry/types'; -import { vi } from 'vitest'; +import { SentrySpan, getTraceData } from '../../../src/'; +import * as SentryCoreTracing from '../../../src/tracing'; -import { getTracingMetaTags, isValidBaggageString } from '../../src/server/meta'; +import { isValidBaggageString } from '../../../src/utils/traceData'; const TRACE_FLAG_SAMPLED = 1; @@ -12,12 +10,6 @@ const mockedSpan = new SentrySpan({ spanId: '1234567890123456', sampled: true, }); -// eslint-disable-next-line deprecation/deprecation -mockedSpan.transaction = { - getDynamicSamplingContext: () => ({ - environment: 'production', - }), -} as Transaction; const mockedClient = {} as any; @@ -27,24 +19,24 @@ const mockedScope = { }), } as any; -describe('getTracingMetaTags', () => { - it('returns the tracing tags from the span, if it is provided', () => { +describe('getTraceData', () => { + it('returns the tracing data from the span, if a span is available', () => { { - vi.spyOn(SentryCore, 'getDynamicSamplingContextFromSpan').mockReturnValueOnce({ + jest.spyOn(SentryCoreTracing, 'getDynamicSamplingContextFromSpan').mockReturnValueOnce({ environment: 'production', }); - const tags = getTracingMetaTags(mockedSpan, mockedScope, mockedClient); + const tags = getTraceData(mockedSpan, mockedScope, mockedClient); expect(tags).toEqual({ - sentryTrace: '', - baggage: '', + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + baggage: 'sentry-environment=production', }); } }); it('returns propagationContext DSC data if no span is available', () => { - const tags = getTracingMetaTags( + const traceData = getTraceData( undefined, { getPropagationContext: () => ({ @@ -61,23 +53,20 @@ describe('getTracingMetaTags', () => { mockedClient, ); - expect(tags).toEqual({ - sentryTrace: expect.stringMatching( - //, - ), - baggage: - '', + expect(traceData).toEqual({ + 'sentry-trace': expect.stringMatching(/12345678901234567890123456789012-(.{16})-1/), + baggage: 'sentry-environment=staging,sentry-public_key=key,sentry-trace_id=12345678901234567890123456789012', }); }); - it('returns only the `sentry-trace` tag if no DSC is available', () => { - vi.spyOn(SentryCore, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({ + it('returns only the `sentry-trace` value if no DSC is available', () => { + jest.spyOn(SentryCoreTracing, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({ trace_id: '', public_key: undefined, }); - const tags = getTracingMetaTags( - // @ts-expect-error - only passing a partial span object + const traceData = getTraceData( + // @ts-expect-error - we don't need to provide all the properties { isRecording: () => true, spanContext: () => { @@ -87,25 +76,24 @@ describe('getTracingMetaTags', () => { traceFlags: TRACE_FLAG_SAMPLED, }; }, - transaction: undefined, }, mockedScope, mockedClient, ); - expect(tags).toEqual({ - sentryTrace: '', + expect(traceData).toEqual({ + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', }); }); it('returns only the `sentry-trace` tag if no DSC is available without a client', () => { - vi.spyOn(SentryCore, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({ + jest.spyOn(SentryCoreTracing, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({ trace_id: '', public_key: undefined, }); - const tags = getTracingMetaTags( - // @ts-expect-error - only passing a partial span object + const traceData = getTraceData( + // @ts-expect-error - we don't need to provide all the properties { isRecording: () => true, spanContext: () => { @@ -115,15 +103,35 @@ describe('getTracingMetaTags', () => { traceFlags: TRACE_FLAG_SAMPLED, }; }, - transaction: undefined, }, mockedScope, undefined, ); - expect(tags).toEqual({ - sentryTrace: '', + expect(traceData).toEqual({ + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', }); + expect('baggage' in traceData).toBe(false); + }); + + it('returns an empty object if the `sentry-trace` value is invalid', () => { + const traceData = getTraceData( + // @ts-expect-error - we don't need to provide all the properties + { + isRecording: () => true, + spanContext: () => { + return { + traceId: '1234567890123456789012345678901+', + spanId: '1234567890123456', + traceFlags: TRACE_FLAG_SAMPLED, + }; + }, + }, + mockedScope, + mockedClient, + ); + + expect(traceData).toEqual({}); }); }); diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index aa30c762d624..69b26bb1729a 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -55,6 +55,7 @@ export { setMeasurement, getActiveSpan, getRootSpan, + getTraceData, startSpan, startInactiveSpan, startSpanManual, diff --git a/packages/feedback/src/core/integration.ts b/packages/feedback/src/core/integration.ts index e2194f43a1d5..888461c9a6bf 100644 --- a/packages/feedback/src/core/integration.ts +++ b/packages/feedback/src/core/integration.ts @@ -209,12 +209,24 @@ export const buildFeedbackIntegration = ({ logger.error('[Feedback] Missing feedback screenshot integration. Proceeding without screenshots.'); } - return modalIntegration.createDialog({ - options, + const dialog = modalIntegration.createDialog({ + options: { + ...options, + onFormClose: () => { + dialog && dialog.close(); + options.onFormClose && options.onFormClose(); + }, + onFormSubmitted: () => { + dialog && dialog.close(); + options.onFormSubmitted && options.onFormSubmitted(); + }, + }, screenshotIntegration: screenshotRequired ? screenshotIntegration : undefined, sendFeedback, shadow: _createShadow(options), }); + + return dialog; }; const _attachTo = (el: Element | string, optionOverrides: OverrideFeedbackConfiguration = {}): Unsubscribe => { @@ -233,10 +245,6 @@ export const buildFeedbackIntegration = ({ if (!dialog) { dialog = await _loadAndRenderDialog({ ...mergedOptions, - onFormClose: () => { - dialog && dialog.close(); - mergedOptions.onFormClose && mergedOptions.onFormClose(); - }, onFormSubmitted: () => { dialog && dialog.removeFromDom(); mergedOptions.onFormSubmitted && mergedOptions.onFormSubmitted(); diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 73e94aa5f271..351f843d2c2d 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -20,6 +20,7 @@ export { getCurrentScope, getGlobalScope, getIsolationScope, + getTraceData, setCurrentClient, Scope, SDK_VERSION, diff --git a/packages/nextjs/README.md b/packages/nextjs/README.md index c7afd15d46c5..03372a8bb6c5 100644 --- a/packages/nextjs/README.md +++ b/packages/nextjs/README.md @@ -10,96 +10,50 @@ [![npm dm](https://img.shields.io/npm/dm/@sentry/nextjs.svg)](https://www.npmjs.com/package/@sentry/nextjs) [![npm dt](https://img.shields.io/npm/dt/@sentry/nextjs.svg)](https://www.npmjs.com/package/@sentry/nextjs) -## Links - -- [Official SDK Docs](https://docs.sentry.io/platforms/javascript/guides/nextjs/) -- [TypeDoc](http://getsentry.github.io/sentry-javascript/) +> See the [Official Sentry Next.js SDK Docs](https://docs.sentry.io/platforms/javascript/guides/nextjs/) to get started. ## Compatibility -Currently, the minimum Next.js supported version is `11.2.0`. - -## General - -This package is a wrapper around `@sentry/node` for the server and `@sentry/react` for the client, with added -functionality related to Next.js. - -To use this SDK, initialize it in the Next.js configuration, in the `sentry.client.config.ts|js` file, and in the -[Next.js Instrumentation Hook](https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation) -(`instrumentation.ts|js`). +Currently, the minimum supported version of Next.js is `13.2.0`. -```javascript -// next.config.js +## Installation -const { withSentryConfig } = require('@sentry/nextjs'); +To get started installing the SDK, use the Sentry Next.js Wizard by running the following command in your terminal or +read the [Getting Started Docs](https://docs.sentry.io/platforms/javascript/guides/nextjs/): -const nextConfig = { - experimental: { - // The instrumentation hook is required for Sentry to work on the serverside - instrumentationHook: true, - }, -}; - -// Wrap the Next.js configuration with Sentry -module.exports = withSentryConfig(nextConfig); +```sh +npx @sentry/wizard@latest -i nextjs ``` -```javascript -// sentry.client.config.js or .ts - -import * as Sentry from '@sentry/nextjs'; +The wizard will prompt you to log in to Sentry. After the wizard setup is completed, the SDK will automatically capture +unhandled errors, and monitor performance. -Sentry.init({ - dsn: '__DSN__', - // Your Sentry configuration for the Browser... -}); -``` +## Custom Usage -```javascript -// instrumentation.ts +To set context information or to send manual events, you can use `@sentry/nextjs` as follows: -import * as Sentry from '@sentry/nextjs'; - -export function register() { - if (process.env.NEXT_RUNTIME === 'nodejs') { - Sentry.init({ - dsn: '__DSN__', - // Your Node.js Sentry configuration... - }); - } - - if (process.env.NEXT_RUNTIME === 'edge') { - Sentry.init({ - dsn: '__DSN__', - // Your Edge Runtime Sentry configuration... - }); - } -} -``` - -To set context information or send manual events, use the exported functions of `@sentry/nextjs`. - -```javascript +```ts import * as Sentry from '@sentry/nextjs'; // Set user information, as well as tags and further extras -Sentry.setExtra('battery', 0.7); Sentry.setTag('user_mode', 'admin'); Sentry.setUser({ id: '4711' }); +Sentry.setContext('application_area', { location: 'checkout' }); // Add a breadcrumb for future events Sentry.addBreadcrumb({ - message: 'My Breadcrumb', + message: '"Add to cart" clicked', // ... }); -// Capture exceptions, messages or manual events +// Capture exceptions or messages +Sentry.captureException(new Error('Oh no.')); Sentry.captureMessage('Hello, world!'); -Sentry.captureException(new Error('Good bye')); -Sentry.captureEvent({ - message: 'Manual', - stacktrace: [ - // ... - ], -}); ``` + +## Links + +- [Official SDK Docs](https://docs.sentry.io/platforms/javascript/guides/nextjs/) +- [Sentry.io](https://sentry.io/?utm_source=github&utm_medium=npm_nextjs) +- [Sentry Discord Server](https://discord.gg/Ww9hbqr) +- [Stack Overflow](https://stackoverflow.com/questions/tagged/sentry) diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 85d001b465e5..badd1f1a27bf 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -18,7 +18,7 @@ export { mongooseIntegration } from './integrations/tracing/mongoose'; export { mysqlIntegration } from './integrations/tracing/mysql'; export { mysql2Integration } from './integrations/tracing/mysql2'; export { redisIntegration } from './integrations/tracing/redis'; -export { nestIntegration, setupNestErrorHandler } from './integrations/tracing/nest'; +export { nestIntegration, setupNestErrorHandler } from './integrations/tracing/nest/nest'; export { postgresIntegration } from './integrations/tracing/postgres'; export { prismaIntegration } from './integrations/tracing/prisma'; export { hapiIntegration, setupHapiErrorHandler } from './integrations/tracing/hapi'; @@ -95,6 +95,7 @@ export { getCurrentHub, getCurrentScope, getIsolationScope, + getTraceData, withScope, withIsolationScope, captureException, diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts index bee4f06db8f5..886c11683674 100644 --- a/packages/node/src/integrations/tracing/index.ts +++ b/packages/node/src/integrations/tracing/index.ts @@ -11,7 +11,7 @@ import { instrumentMongo, mongoIntegration } from './mongo'; import { instrumentMongoose, mongooseIntegration } from './mongoose'; import { instrumentMysql, mysqlIntegration } from './mysql'; import { instrumentMysql2, mysql2Integration } from './mysql2'; -import { instrumentNest, nestIntegration } from './nest'; +import { instrumentNest, nestIntegration } from './nest/nest'; import { instrumentPostgres, postgresIntegration } from './postgres'; import { instrumentRedis, redisIntegration } from './redis'; diff --git a/packages/node/src/integrations/tracing/nest/helpers.ts b/packages/node/src/integrations/tracing/nest/helpers.ts new file mode 100644 index 000000000000..32eb3a0d5a39 --- /dev/null +++ b/packages/node/src/integrations/tracing/nest/helpers.ts @@ -0,0 +1,34 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { addNonEnumerableProperty } from '@sentry/utils'; +import type { InjectableTarget } from './types'; + +const sentryPatched = 'sentryPatched'; + +/** + * Helper checking if a concrete target class is already patched. + * + * We already guard duplicate patching with isWrapped. However, isWrapped checks whether a file has been patched, whereas we use this check for concrete target classes. + * This check might not be necessary, but better to play it safe. + */ +export function isPatched(target: InjectableTarget): boolean { + if (target.sentryPatched) { + return true; + } + + addNonEnumerableProperty(target, sentryPatched, true); + return false; +} + +/** + * Returns span options for nest middleware spans. + */ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function getMiddlewareSpanOptions(target: InjectableTarget) { + return { + name: target.name, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nestjs', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nestjs', + }, + }; +} diff --git a/packages/node/src/integrations/tracing/nest/nest.ts b/packages/node/src/integrations/tracing/nest/nest.ts new file mode 100644 index 000000000000..4f7f7a1f59d3 --- /dev/null +++ b/packages/node/src/integrations/tracing/nest/nest.ts @@ -0,0 +1,123 @@ +import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + captureException, + defineIntegration, + getClient, + getDefaultIsolationScope, + getIsolationScope, + spanToJSON, +} from '@sentry/core'; +import type { IntegrationFn, Span } from '@sentry/types'; +import { logger } from '@sentry/utils'; +import { generateInstrumentOnce } from '../../../otel/instrument'; +import { SentryNestInstrumentation } from './sentry-nest-instrumentation'; +import type { MinimalNestJsApp, NestJsErrorFilter } from './types'; + +const INTEGRATION_NAME = 'Nest'; + +const instrumentNestCore = generateInstrumentOnce('Nest-Core', () => { + return new NestInstrumentation(); +}); + +const instrumentNestCommon = generateInstrumentOnce('Nest-Common', () => { + return new SentryNestInstrumentation(); +}); + +export const instrumentNest = Object.assign( + (): void => { + instrumentNestCore(); + instrumentNestCommon(); + }, + { id: INTEGRATION_NAME }, +); + +const _nestIntegration = (() => { + return { + name: INTEGRATION_NAME, + setupOnce() { + instrumentNest(); + }, + }; +}) satisfies IntegrationFn; + +/** + * Nest framework integration + * + * Capture tracing data for nest. + */ +export const nestIntegration = defineIntegration(_nestIntegration); + +/** + * Setup an error handler for Nest. + */ +export function setupNestErrorHandler(app: MinimalNestJsApp, baseFilter: NestJsErrorFilter): void { + // Sadly, NestInstrumentation has no requestHook, so we need to add the attributes here + // We register this hook in this method, because if we register it in the integration `setup`, + // it would always run even for users that are not even using Nest.js + const client = getClient(); + if (client) { + client.on('spanStart', span => { + addNestSpanAttributes(span); + }); + } + + app.useGlobalInterceptors({ + intercept(context, next) { + if (getIsolationScope() === getDefaultIsolationScope()) { + logger.warn('Isolation scope is still the default isolation scope, skipping setting transactionName.'); + return next.handle(); + } + + if (context.getType() === 'http') { + const req = context.switchToHttp().getRequest(); + if (req.route) { + getIsolationScope().setTransactionName(`${req.method?.toUpperCase() || 'GET'} ${req.route.path}`); + } + } + + return next.handle(); + }, + }); + + const wrappedFilter = new Proxy(baseFilter, { + get(target, prop, receiver) { + if (prop === 'catch') { + const originalCatch = Reflect.get(target, prop, receiver); + + return (exception: unknown, host: unknown) => { + const status_code = (exception as { status?: number }).status; + + // don't report expected errors + if (status_code !== undefined) { + return originalCatch.apply(target, [exception, host]); + } + + captureException(exception); + return originalCatch.apply(target, [exception, host]); + }; + } + return Reflect.get(target, prop, receiver); + }, + }); + + app.useGlobalFilters(wrappedFilter); +} + +function addNestSpanAttributes(span: Span): void { + const attributes = spanToJSON(span).data || {}; + + // this is one of: app_creation, request_context, handler + const type = attributes['nestjs.type']; + + // If this is already set, or we have no nest.js span, no need to process again... + if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] || !type) { + return; + } + + span.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.nestjs', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.nestjs`, + }); +} diff --git a/packages/node/src/integrations/tracing/nest.ts b/packages/node/src/integrations/tracing/nest/sentry-nest-instrumentation.ts similarity index 51% rename from packages/node/src/integrations/tracing/nest.ts rename to packages/node/src/integrations/tracing/nest/sentry-nest-instrumentation.ts index b3d1b3547118..52c3a4ad6b40 100644 --- a/packages/node/src/integrations/tracing/nest.ts +++ b/packages/node/src/integrations/tracing/nest/sentry-nest-instrumentation.ts @@ -5,121 +5,14 @@ import { InstrumentationNodeModuleDefinition, InstrumentationNodeModuleFile, } from '@opentelemetry/instrumentation'; -import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core'; -import { - SDK_VERSION, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - captureException, - defineIntegration, - getActiveSpan, - getClient, - getDefaultIsolationScope, - getIsolationScope, - spanToJSON, - startSpan, - startSpanManual, - withActiveSpan, -} from '@sentry/core'; -import type { IntegrationFn, Span } from '@sentry/types'; -import { addNonEnumerableProperty, logger } from '@sentry/utils'; -import { generateInstrumentOnce } from '../../otel/instrument'; - -interface MinimalNestJsExecutionContext { - getType: () => string; - - switchToHttp: () => { - // minimal request object - // according to official types, all properties are required but - // let's play it safe and assume they're optional - getRequest: () => { - route?: { - path?: string; - }; - method?: string; - }; - }; -} - -interface NestJsErrorFilter { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - catch(exception: any, host: any): void; -} - -interface MinimalNestJsApp { - useGlobalFilters: (arg0: NestJsErrorFilter) => void; - useGlobalInterceptors: (interceptor: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - intercept: (context: MinimalNestJsExecutionContext, next: { handle: () => any }) => any; - }) => void; -} - -const INTEGRATION_NAME = 'Nest'; +import { getActiveSpan, startSpan, startSpanManual, withActiveSpan } from '@sentry/core'; +import type { Span } from '@sentry/types'; +import { SDK_VERSION } from '@sentry/utils'; +import { getMiddlewareSpanOptions, isPatched } from './helpers'; +import type { InjectableTarget } from './types'; const supportedVersions = ['>=8.0.0 <11']; -const sentryPatched = 'sentryPatched'; - -/** - * A minimal interface for an Observable. - */ -export interface Observable { - subscribe(observer: (value: T) => void): void; -} - -/** - * A NestJS call handler. Used in interceptors to start the route execution. - */ -export interface CallHandler { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - handle(...args: any[]): Observable; -} - -/** - * Represents an injectable target class in NestJS. - */ -export interface InjectableTarget { - name: string; - sentryPatched?: boolean; - __SENTRY_INTERNAL__?: boolean; - prototype: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - use?: (req: unknown, res: unknown, next: () => void, ...args: any[]) => void; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - canActivate?: (...args: any[]) => boolean | Promise | Observable; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - transform?: (...args: any[]) => any; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - intercept?: (context: unknown, next: CallHandler, ...args: any[]) => Observable; - }; -} - -/** - * Helper checking if a concrete target class is already patched. - * - * We already guard duplicate patching with isWrapped. However, isWrapped checks whether a file has been patched, whereas we use this check for concrete target classes. - * This check might not be necessary, but better to play it safe. - */ -export function isPatched(target: InjectableTarget): boolean { - if (target.sentryPatched) { - return true; - } - - addNonEnumerableProperty(target, sentryPatched, true); - return false; -} - -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -function getMiddlewareSpanOptions(target: InjectableTarget) { - return { - name: target.name, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nestjs', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nestjs', - }, - }; -} - /** * Custom instrumentation for nestjs. * @@ -285,108 +178,3 @@ export class SentryNestInstrumentation extends InstrumentationBase { }; } } - -const instrumentNestCore = generateInstrumentOnce('Nest-Core', () => { - return new NestInstrumentation(); -}); - -const instrumentNestCommon = generateInstrumentOnce('Nest-Common', () => { - return new SentryNestInstrumentation(); -}); - -export const instrumentNest = Object.assign( - (): void => { - instrumentNestCore(); - instrumentNestCommon(); - }, - { id: INTEGRATION_NAME }, -); - -const _nestIntegration = (() => { - return { - name: INTEGRATION_NAME, - setupOnce() { - instrumentNest(); - }, - }; -}) satisfies IntegrationFn; - -/** - * Nest framework integration - * - * Capture tracing data for nest. - */ -export const nestIntegration = defineIntegration(_nestIntegration); - -/** - * Setup an error handler for Nest. - */ -export function setupNestErrorHandler(app: MinimalNestJsApp, baseFilter: NestJsErrorFilter): void { - // Sadly, NestInstrumentation has no requestHook, so we need to add the attributes here - // We register this hook in this method, because if we register it in the integration `setup`, - // it would always run even for users that are not even using Nest.js - const client = getClient(); - if (client) { - client.on('spanStart', span => { - addNestSpanAttributes(span); - }); - } - - app.useGlobalInterceptors({ - intercept(context, next) { - if (getIsolationScope() === getDefaultIsolationScope()) { - logger.warn('Isolation scope is still the default isolation scope, skipping setting transactionName.'); - return next.handle(); - } - - if (context.getType() === 'http') { - const req = context.switchToHttp().getRequest(); - if (req.route) { - getIsolationScope().setTransactionName(`${req.method?.toUpperCase() || 'GET'} ${req.route.path}`); - } - } - - return next.handle(); - }, - }); - - const wrappedFilter = new Proxy(baseFilter, { - get(target, prop, receiver) { - if (prop === 'catch') { - const originalCatch = Reflect.get(target, prop, receiver); - - return (exception: unknown, host: unknown) => { - const status_code = (exception as { status?: number }).status; - - // don't report expected errors - if (status_code !== undefined) { - return originalCatch.apply(target, [exception, host]); - } - - captureException(exception); - return originalCatch.apply(target, [exception, host]); - }; - } - return Reflect.get(target, prop, receiver); - }, - }); - - app.useGlobalFilters(wrappedFilter); -} - -function addNestSpanAttributes(span: Span): void { - const attributes = spanToJSON(span).data || {}; - - // this is one of: app_creation, request_context, handler - const type = attributes['nestjs.type']; - - // If this is already set, or we have no nest.js span, no need to process again... - if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] || !type) { - return; - } - - span.setAttributes({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.nestjs', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.nestjs`, - }); -} diff --git a/packages/node/src/integrations/tracing/nest/types.ts b/packages/node/src/integrations/tracing/nest/types.ts new file mode 100644 index 000000000000..2cdd1b6aefaf --- /dev/null +++ b/packages/node/src/integrations/tracing/nest/types.ts @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +interface MinimalNestJsExecutionContext { + getType: () => string; + + switchToHttp: () => { + // minimal request object + // according to official types, all properties are required but + // let's play it safe and assume they're optional + getRequest: () => { + route?: { + path?: string; + }; + method?: string; + }; + }; +} + +export interface NestJsErrorFilter { + catch(exception: any, host: any): void; +} + +export interface MinimalNestJsApp { + useGlobalFilters: (arg0: NestJsErrorFilter) => void; + useGlobalInterceptors: (interceptor: { + intercept: (context: MinimalNestJsExecutionContext, next: { handle: () => any }) => any; + }) => void; +} + +/** + * A minimal interface for an Observable. + */ +export interface Observable { + subscribe(observer: (value: T) => void): void; +} + +/** + * A NestJS call handler. Used in interceptors to start the route execution. + */ +export interface CallHandler { + handle(...args: any[]): Observable; +} + +/** + * Represents an injectable target class in NestJS. + */ +export interface InjectableTarget { + name: string; + sentryPatched?: boolean; + __SENTRY_INTERNAL__?: boolean; + prototype: { + use?: (req: unknown, res: unknown, next: () => void, ...args: any[]) => void; + canActivate?: (...args: any[]) => boolean | Promise | Observable; + transform?: (...args: any[]) => any; + intercept?: (context: unknown, next: CallHandler, ...args: any[]) => Observable; + }; +} diff --git a/packages/node/test/integrations/tracing/nest.test.ts b/packages/node/test/integrations/tracing/nest.test.ts index 3dc321f28008..3837e3e4ee3d 100644 --- a/packages/node/test/integrations/tracing/nest.test.ts +++ b/packages/node/test/integrations/tracing/nest.test.ts @@ -1,5 +1,5 @@ -import type { InjectableTarget } from '../../../src/integrations/tracing/nest'; -import { isPatched } from '../../../src/integrations/tracing/nest'; +import { isPatched } from '../../../src/integrations/tracing/nest/helpers'; +import type { InjectableTarget } from '../../../src/integrations/tracing/nest/types'; describe('Nest', () => { describe('isPatched', () => { diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 9d2575febfe2..cab0b19e1fbe 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -30,9 +30,9 @@ } }, "./module": { - "types": "./build/types/module/index.d.ts", - "import": "./build/esm/module/index.js", - "require": "./build/cjs/module/index.js" + "types": "./build/module/types.d.ts", + "import": "./build/module/module.mjs", + "require": "./build/module/module.cjs" } }, "publishConfig": { @@ -53,12 +53,14 @@ "@sentry/vue": "8.22.0" }, "devDependencies": { + "@nuxt/module-builder": "0.8.1", "nuxt": "^3.12.2" }, "scripts": { "build": "run-s build:types build:transpile", "build:dev": "yarn build", - "build:transpile": "rollup -c rollup.npm.config.mjs", + "build:nuxt-module": "nuxt-module-build build --outDir build/module", + "build:transpile": "rollup -c rollup.npm.config.mjs && yarn build:nuxt-module", "build:types": "tsc -p tsconfig.types.json", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "yarn build:watch", @@ -88,8 +90,7 @@ "outputs": [ "{projectRoot}/build/cjs", "{projectRoot}/build/esm", - "{projectRoot}/build/cjs/module", - "{projectRoot}/build/esm/module" + "{projectRoot}/build/module" ] } } diff --git a/packages/nuxt/rollup.npm.config.mjs b/packages/nuxt/rollup.npm.config.mjs index 63db9a45e9d6..d124ba8a7844 100644 --- a/packages/nuxt/rollup.npm.config.mjs +++ b/packages/nuxt/rollup.npm.config.mjs @@ -8,28 +8,27 @@ export default [ 'src/index.client.ts', 'src/client/index.ts', 'src/server/index.ts', - 'src/module/index.ts', + 'src/module.ts', ], packageSpecificConfig: { external: ['nuxt/app'], }, }), ), + /* The Nuxt module plugins are also built with the @nuxt/module-builder. + This rollup setup is still left here for an easier switch between the setups while + manually testing different built outputs (module-builder vs. rollup only) */ ...makeNPMConfigVariants( makeBaseNPMConfig({ - entrypoints: ['src/module/plugins/sentry.client.ts', 'src/module/plugins/sentry.server.ts'], + entrypoints: ['src/runtime/plugins/sentry.client.ts', 'src/runtime/plugins/sentry.server.ts'], packageSpecificConfig: { external: ['nuxt/app', 'nitropack/runtime', 'h3'], output: { // Preserve the original file structure (i.e., so that everything is still relative to `src`) - entryFileNames: 'module/[name].js', + entryFileNames: 'runtime/[name].js', }, }, }), ), ]; - -/* - - */ diff --git a/packages/nuxt/src/module/index.ts b/packages/nuxt/src/module.ts similarity index 87% rename from packages/nuxt/src/module/index.ts rename to packages/nuxt/src/module.ts index 2b38d413d06f..6cfccfbd2714 100644 --- a/packages/nuxt/src/module/index.ts +++ b/packages/nuxt/src/module.ts @@ -1,8 +1,8 @@ import * as fs from 'fs'; import * as path from 'path'; import { addPlugin, addPluginTemplate, addServerPlugin, createResolver, defineNuxtModule } from '@nuxt/kit'; -import type { SentryNuxtModuleOptions } from '../common/types'; -import { setupSourceMaps } from '../vite/sourceMaps'; +import type { SentryNuxtModuleOptions } from './common/types'; +import { setupSourceMaps } from './vite/sourceMaps'; export type ModuleOptions = SentryNuxtModuleOptions; @@ -31,7 +31,7 @@ export default defineNuxtModule({ 'export default defineNuxtPlugin(() => {})', }); - addPlugin({ src: moduleDirResolver.resolve('./plugins/sentry.client'), mode: 'client' }); + addPlugin({ src: moduleDirResolver.resolve('./runtime/plugins/sentry.client'), mode: 'client' }); } const serverConfigFile = findDefaultSdkInitFile('server'); @@ -46,7 +46,7 @@ export default defineNuxtModule({ 'export default defineNuxtPlugin(() => {})', }); - addServerPlugin(moduleDirResolver.resolve('./plugins/sentry.server')); + addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/sentry.server')); } if (clientConfigFile || serverConfigFile) { diff --git a/packages/nuxt/src/module/plugins/sentry.client.ts b/packages/nuxt/src/runtime/plugins/sentry.client.ts similarity index 100% rename from packages/nuxt/src/module/plugins/sentry.client.ts rename to packages/nuxt/src/runtime/plugins/sentry.client.ts diff --git a/packages/nuxt/src/module/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts similarity index 100% rename from packages/nuxt/src/module/plugins/sentry.server.ts rename to packages/nuxt/src/runtime/plugins/sentry.server.ts diff --git a/packages/nuxt/src/module/utils.ts b/packages/nuxt/src/runtime/utils.ts similarity index 100% rename from packages/nuxt/src/module/utils.ts rename to packages/nuxt/src/runtime/utils.ts diff --git a/packages/nuxt/src/server/sdk.ts b/packages/nuxt/src/server/sdk.ts index f14cc23ab8cd..deadea3c54df 100644 --- a/packages/nuxt/src/server/sdk.ts +++ b/packages/nuxt/src/server/sdk.ts @@ -1,6 +1,8 @@ import { applySdkMetadata, getGlobalScope } from '@sentry/core'; import { init as initNode } from '@sentry/node'; import type { Client, EventProcessor } from '@sentry/types'; +import { logger } from '@sentry/utils'; +import { DEBUG_BUILD } from '../common/debug-build'; import type { SentryNuxtOptions } from '../common/types'; /** @@ -26,8 +28,8 @@ export function init(options: SentryNuxtOptions): Client | undefined { // todo: the buildAssetDir could be changed in the nuxt config - change this to a more generic solution if (event.transaction?.match(/^GET \/_nuxt\//)) { options.debug && - // eslint-disable-next-line no-console - console.log('[Sentry] NuxtLowQualityTransactionsFilter filtered transaction: ', event.transaction); + DEBUG_BUILD && + logger.log('NuxtLowQualityTransactionsFilter filtered transaction: ', event.transaction); return null; } diff --git a/packages/nuxt/test/client/runtime/utils.test.ts b/packages/nuxt/test/client/runtime/utils.test.ts index ceb10b9bb7fa..b0b039d52e54 100644 --- a/packages/nuxt/test/client/runtime/utils.test.ts +++ b/packages/nuxt/test/client/runtime/utils.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { extractErrorContext } from '../../../src/module/utils'; +import { extractErrorContext } from '../../../src/runtime/utils'; describe('extractErrorContext', () => { it('returns empty object for undefined or empty context', () => { diff --git a/packages/nuxt/test/server/runtime/plugin.test.ts b/packages/nuxt/test/server/runtime/plugin.test.ts index 407eec41eb59..518b20026cbd 100644 --- a/packages/nuxt/test/server/runtime/plugin.test.ts +++ b/packages/nuxt/test/server/runtime/plugin.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import { addSentryTracingMetaTags } from '../../../src/module/utils'; +import { addSentryTracingMetaTags } from '../../../src/runtime/utils'; const mockReturns = vi.hoisted(() => { return { diff --git a/packages/profiling-node/bindings/cpu_profiler.cc b/packages/profiling-node/bindings/cpu_profiler.cc index 9cda97d46b40..00996db9e8c9 100644 --- a/packages/profiling-node/bindings/cpu_profiler.cc +++ b/packages/profiling-node/bindings/cpu_profiler.cc @@ -28,6 +28,7 @@ enum ProfileFormat { kFormatThread = 0, kFormatChunk = 1, }; + // Allow users to override the default logging mode via env variable. This is // useful because sometimes the flow of the profiled program can be to execute // many sequential transaction - in that case, it may be preferable to set eager diff --git a/packages/solidstart/README.md b/packages/solidstart/README.md index e27e73447f2d..61aa3b2793da 100644 --- a/packages/solidstart/README.md +++ b/packages/solidstart/README.md @@ -46,10 +46,12 @@ Initialize the SDK in `entry-client.jsx` ```jsx import * as Sentry from '@sentry/solidstart'; +import { solidRouterBrowserTracingIntegration } from '@sentry/solidstart/solidrouter'; import { mount, StartClient } from '@solidjs/start/client'; Sentry.init({ dsn: '__PUBLIC_DSN__', + integrations: [solidRouterBrowserTracingIntegration()], tracesSampleRate: 1.0, // Capture 100% of the transactions }); @@ -69,7 +71,37 @@ Sentry.init({ }); ``` -### 4. Run your application +### 4. Server instrumentation + +Complete the setup by adding the Sentry middlware to your `src/middleware.ts` file: + +```typescript +import { sentryBeforeResponseMiddleware } from '@sentry/solidstart/middleware'; +import { createMiddleware } from '@solidjs/start/middleware'; + +export default createMiddleware({ + onBeforeResponse: [ + sentryBeforeResponseMiddleware(), + // Add your other middleware handlers after `sentryBeforeResponseMiddleware` + ], +}); +``` + +And don't forget to specify `./src/middleware.ts` in your `app.config.ts`: + +```typescript +import { defineConfig } from '@solidjs/start/config'; + +export default defineConfig({ + // ... + middleware: './src/middleware.ts', +}); +``` + +The Sentry middleware enhances the data collected by Sentry on the server side by enabling distributed tracing between +the client and server. + +### 5. Run your application Then run your app diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index 7a6e1849b589..785cef7fc94e 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -39,6 +39,17 @@ "require": "./build/cjs/index.server.js" } }, + "./middleware": { + "types": "./middleware.d.ts", + "import": { + "types": "./middleware.d.ts", + "default": "./build/esm/middleware.js" + }, + "require": { + "types": "./middleware.d.ts", + "default": "./build/cjs/middleware.js" + } + }, "./solidrouter": { "types": "./solidrouter.d.ts", "browser": { @@ -87,15 +98,15 @@ "build": "run-p build:transpile build:types", "build:dev": "yarn build", "build:transpile": "rollup -c rollup.npm.config.mjs", - "build:types": "run-s build:types:core build:types:solidrouter", + "build:types": "run-s build:types:core build:types:subexports", "build:types:core": "tsc -p tsconfig.types.json", - "build:types:solidrouter": "tsc -p tsconfig.solidrouter-types.json", + "build:types:subexports": "tsc -p tsconfig.subexports-types.json", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "yarn build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "npm pack", - "circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts && madge --circular src/solidrouter.client.ts && madge --circular src/solidrouter.server.ts && madge --circular src/solidrouter.ts", + "circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts && madge --circular src/solidrouter.client.ts && madge --circular src/solidrouter.server.ts && madge --circular src/solidrouter.ts && madge --circular src/middleware.ts", "clean": "rimraf build coverage sentry-solidstart-*.tgz ./*.d.ts ./*.d.ts.map ./client ./server", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", diff --git a/packages/solidstart/rollup.npm.config.mjs b/packages/solidstart/rollup.npm.config.mjs index b0087a93c6fe..8e91d0371a27 100644 --- a/packages/solidstart/rollup.npm.config.mjs +++ b/packages/solidstart/rollup.npm.config.mjs @@ -12,6 +12,7 @@ export default makeNPMConfigVariants( 'src/solidrouter.server.ts', 'src/client/solidrouter.ts', 'src/server/solidrouter.ts', + 'src/middleware.ts', ], // prevent this internal code from ending up in our built package (this doesn't happen automatially because // the name doesn't match an SDK dependency) diff --git a/packages/solidstart/src/middleware.ts b/packages/solidstart/src/middleware.ts new file mode 100644 index 000000000000..0113cce8f988 --- /dev/null +++ b/packages/solidstart/src/middleware.ts @@ -0,0 +1,61 @@ +import { getTraceData } from '@sentry/core'; +import { addNonEnumerableProperty } from '@sentry/utils'; +import type { ResponseMiddleware } from '@solidjs/start/middleware'; +import type { FetchEvent } from '@solidjs/start/server'; + +export type ResponseMiddlewareResponse = Parameters[1] & { + __sentry_wrapped__?: boolean; +}; + +function addMetaTagToHead(html: string): string { + const { 'sentry-trace': sentryTrace, baggage } = getTraceData(); + + if (!sentryTrace) { + return html; + } + + const metaTags = [``]; + + if (baggage) { + metaTags.push(``); + } + + const content = `\n${metaTags.join('\n')}\n`; + return html.replace('', content); +} + +/** + * Returns an `onBeforeResponse` solid start middleware handler that adds tracing data as + * tags to a page on pageload to enable distributed tracing. + */ +export function sentryBeforeResponseMiddleware() { + return async function onBeforeResponse(event: FetchEvent, response: ResponseMiddlewareResponse) { + if (!response.body || response.__sentry_wrapped__) { + return; + } + + // Ensure we don't double-wrap, in case a user has added the middleware twice + // e.g. once manually, once via the wizard + addNonEnumerableProperty(response, '__sentry_wrapped__', true); + + const contentType = event.response.headers.get('content-type'); + const isPageloadRequest = contentType && contentType.startsWith('text/html'); + + if (!isPageloadRequest) { + return; + } + + const body = response.body as NodeJS.ReadableStream; + const decoder = new TextDecoder(); + response.body = new ReadableStream({ + start: async controller => { + for await (const chunk of body) { + const html = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true }); + const modifiedHtml = addMetaTagToHead(html); + controller.enqueue(new TextEncoder().encode(modifiedHtml)); + } + controller.close(); + }, + }); + }; +} diff --git a/packages/solidstart/src/server/sdk.ts b/packages/solidstart/src/server/sdk.ts index 7329100d9de9..883d2a0ef63f 100644 --- a/packages/solidstart/src/server/sdk.ts +++ b/packages/solidstart/src/server/sdk.ts @@ -1,6 +1,7 @@ import { applySdkMetadata } from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; import { init as initNodeSdk } from '@sentry/node'; +import { filterLowQualityTransactions } from './utils'; /** * Initializes the server side of the Solid Start SDK @@ -11,6 +12,7 @@ export function init(options: NodeOptions): NodeClient | undefined { }; applySdkMetadata(opts, 'solidstart', ['solidstart', 'node']); + filterLowQualityTransactions(opts); return initNodeSdk(opts); } diff --git a/packages/solidstart/src/server/utils.ts b/packages/solidstart/src/server/utils.ts index f3d26e5d3a26..f570ae355424 100644 --- a/packages/solidstart/src/server/utils.ts +++ b/packages/solidstart/src/server/utils.ts @@ -1,4 +1,5 @@ -import { flush } from '@sentry/node'; +import { flush, getGlobalScope } from '@sentry/node'; +import type { EventProcessor, Options } from '@sentry/types'; import { logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../common/debug-build'; @@ -31,3 +32,26 @@ export function isRedirect(error: unknown): boolean { const hasValidStatus = error.status >= 300 && error.status <= 308; return hasValidLocation && hasValidStatus; } + +/** + * Adds an event processor to filter out low quality transactions, + * e.g. to filter out transactions for build assets + */ +export function filterLowQualityTransactions(options: Options): void { + getGlobalScope().addEventProcessor( + Object.assign( + (event => { + if (event.type !== 'transaction') { + return event; + } + // Filter out transactions for build assets + if (event.transaction?.match(/^GET \/_build\//)) { + options.debug && logger.log('SolidStartLowQualityTransactionsFilter filtered transaction', event.transaction); + return null; + } + return event; + }) satisfies EventProcessor, + { id: 'SolidStartLowQualityTransactionsFilter' }, + ), + ); +} diff --git a/packages/solidstart/test/middleware.test.ts b/packages/solidstart/test/middleware.test.ts new file mode 100644 index 000000000000..888a0fbc702d --- /dev/null +++ b/packages/solidstart/test/middleware.test.ts @@ -0,0 +1,82 @@ +import * as SentryCore from '@sentry/core'; +import { beforeEach, describe, it, vi } from 'vitest'; +import { sentryBeforeResponseMiddleware } from '../src/middleware'; +import type { ResponseMiddlewareResponse } from '../src/middleware'; + +describe('middleware', () => { + describe('sentryBeforeResponseMiddleware', () => { + vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({ + 'sentry-trace': '123', + baggage: 'abc', + }); + + const mockFetchEvent = { + request: {}, + locals: {}, + response: { + // mocks a pageload + headers: new Headers([['content-type', 'text/html']]), + }, + nativeEvent: {}, + }; + + let mockMiddlewareHTMLResponse: ResponseMiddlewareResponse; + let mockMiddlewareHTMLNoHeadResponse: ResponseMiddlewareResponse; + let mockMiddlewareJSONResponse: ResponseMiddlewareResponse; + + beforeEach(() => { + // h3 doesn't pass a proper Response object to the middleware + mockMiddlewareHTMLResponse = { + body: new Response('').body, + }; + mockMiddlewareHTMLNoHeadResponse = { + body: new Response('Hello World').body, + }; + mockMiddlewareJSONResponse = { + body: new Response('{"prefecture": "Kagoshima"}').body, + }; + }); + + it('injects tracing meta tags into the response body', async () => { + const onBeforeResponse = sentryBeforeResponseMiddleware(); + onBeforeResponse(mockFetchEvent, mockMiddlewareHTMLResponse); + + // for testing convenience, we pass the body back into a proper response + // mockMiddlewareHTMLResponse has been modified by our middleware + const html = await new Response(mockMiddlewareHTMLResponse.body).text(); + expect(html).toContain(''); + expect(html).toContain(''); + expect(html).toContain(''); + }); + + it('does not add meta tags if there is no head tag', async () => { + const onBeforeResponse = sentryBeforeResponseMiddleware(); + onBeforeResponse(mockFetchEvent, mockMiddlewareHTMLNoHeadResponse); + + const html = await new Response(mockMiddlewareHTMLNoHeadResponse.body).text(); + expect(html).toEqual('Hello World'); + }); + + it('does not add tracing meta tags twice into the same response', async () => { + const onBeforeResponse1 = sentryBeforeResponseMiddleware(); + onBeforeResponse1(mockFetchEvent, mockMiddlewareHTMLResponse); + + const onBeforeResponse2 = sentryBeforeResponseMiddleware(); + onBeforeResponse2(mockFetchEvent, mockMiddlewareHTMLResponse); + + const html = await new Response(mockMiddlewareHTMLResponse.body).text(); + expect(html.match(//g)).toHaveLength(1); + expect(html.match(//g)).toHaveLength(1); + }); + + it('does not modify a non-HTML response', async () => { + const onBeforeResponse = sentryBeforeResponseMiddleware(); + onBeforeResponse({ ...mockFetchEvent, response: { headers: new Headers() } }, mockMiddlewareJSONResponse); + + const json = await new Response(mockMiddlewareJSONResponse.body).json(); + expect(json).toEqual({ + prefecture: 'Kagoshima', + }); + }); + }); +}); diff --git a/packages/solidstart/test/server/sdk.test.ts b/packages/solidstart/test/server/sdk.test.ts index e658876c0a12..b700b43a067a 100644 --- a/packages/solidstart/test/server/sdk.test.ts +++ b/packages/solidstart/test/server/sdk.test.ts @@ -1,7 +1,7 @@ +import type { NodeClient } from '@sentry/node'; import { SDK_VERSION } from '@sentry/node'; import * as SentryNode from '@sentry/node'; - -import { vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { init as solidStartInit } from '../../src/server'; const browserInit = vi.spyOn(SentryNode, 'init'); @@ -33,4 +33,38 @@ describe('Initialize Solid Start SDK', () => { expect(browserInit).toHaveBeenCalledTimes(1); expect(browserInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata)); }); + + it('filters out low quality transactions', async () => { + const beforeSendEvent = vi.fn(event => event); + const client = solidStartInit({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + }) as NodeClient; + client.on('beforeSendEvent', beforeSendEvent); + + client.captureEvent({ type: 'transaction', transaction: 'GET /' }); + client.captureEvent({ type: 'transaction', transaction: 'GET /_build/some_asset.js' }); + client.captureEvent({ type: 'transaction', transaction: 'POST /_server' }); + + await client!.flush(); + + expect(beforeSendEvent).toHaveBeenCalledTimes(2); + expect(beforeSendEvent).toHaveBeenCalledWith( + expect.objectContaining({ + transaction: 'GET /', + }), + expect.any(Object), + ); + expect(beforeSendEvent).not.toHaveBeenCalledWith( + expect.objectContaining({ + transaction: 'GET /_build/some_asset.js', + }), + expect.any(Object), + ); + expect(beforeSendEvent).toHaveBeenCalledWith( + expect.objectContaining({ + transaction: 'POST /_server', + }), + expect.any(Object), + ); + }); }); diff --git a/packages/solidstart/tsconfig.solidrouter-types.json b/packages/solidstart/tsconfig.subexports-types.json similarity index 95% rename from packages/solidstart/tsconfig.solidrouter-types.json rename to packages/solidstart/tsconfig.subexports-types.json index f800d830c511..1c9daec11314 100644 --- a/packages/solidstart/tsconfig.solidrouter-types.json +++ b/packages/solidstart/tsconfig.subexports-types.json @@ -15,6 +15,7 @@ "src/solidrouter.server.ts", "src/server/solidrouter.ts", "src/solidrouter.ts", + "src/middleware.ts", ], // Without this, we cannot output into the root dir "exclude": [] diff --git a/packages/solidstart/tsconfig.types.json b/packages/solidstart/tsconfig.types.json index f7cc8c3d1610..bf2ca092abc1 100644 --- a/packages/solidstart/tsconfig.types.json +++ b/packages/solidstart/tsconfig.types.json @@ -14,6 +14,7 @@ "src/client/solidrouter.ts", "src/solidrouter.server.ts", "src/server/solidrouter.ts", - "src/solidrouter.ts" + "src/solidrouter.ts", + "src/middleware.ts", ] } diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 3a14771218e4..a74e5bb89dc0 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -51,6 +51,7 @@ export { getSentryRelease, getSpanDescendants, getSpanStatusFromHttpCode, + getTraceData, graphqlIntegration, hapiIntegration, httpIntegration, diff --git a/packages/types/src/eventprocessor.ts b/packages/types/src/eventprocessor.ts index 60a983fa0fdc..54177388cdde 100644 --- a/packages/types/src/eventprocessor.ts +++ b/packages/types/src/eventprocessor.ts @@ -1,7 +1,7 @@ import type { Event, EventHint } from './event'; /** - * Event processors are used to change the event before it will be send. + * Event processors are used to change the event before it will be sent. * We strongly advise to make this function sync. * Returning a PromiseLike will work just fine, but better be sure that you know what you are doing. * Event processing will be deferred until your Promise is resolved. diff --git a/packages/utils/src/instrument/fetch.ts b/packages/utils/src/instrument/fetch.ts index 0ea1a4ec8d9f..afa209c01929 100644 --- a/packages/utils/src/instrument/fetch.ts +++ b/packages/utils/src/instrument/fetch.ts @@ -80,47 +80,42 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void, skipNat if (onFetchResolved) { onFetchResolved(response); } else { - const finishedHandlerData: HandlerDataFetch = { + triggerHandlers('fetch', { ...handlerData, endTimestamp: timestampInSeconds() * 1000, response, - }; - triggerHandlers('fetch', finishedHandlerData); + }); } return response; }, (error: Error) => { - if (!onFetchResolved) { - const erroredHandlerData: HandlerDataFetch = { - ...handlerData, - endTimestamp: timestampInSeconds() * 1000, - error, - }; - - triggerHandlers('fetch', erroredHandlerData); - - if (isError(error) && error.stack === undefined) { - // NOTE: If you are a Sentry user, and you are seeing this stack frame, - // it means the error, that was caused by your fetch call did not - // have a stack trace, so the SDK backfilled the stack trace so - // you can see which fetch call failed. - error.stack = virtualStackTrace; - addNonEnumerableProperty(error, 'framesToPop', 1); - } + triggerHandlers('fetch', { + ...handlerData, + endTimestamp: timestampInSeconds() * 1000, + error, + }); + if (isError(error) && error.stack === undefined) { // NOTE: If you are a Sentry user, and you are seeing this stack frame, - // it means the sentry.javascript SDK caught an error invoking your application code. - // This is expected behavior and NOT indicative of a bug with sentry.javascript. - throw error; + // it means the error, that was caused by your fetch call did not + // have a stack trace, so the SDK backfilled the stack trace so + // you can see which fetch call failed. + error.stack = virtualStackTrace; + addNonEnumerableProperty(error, 'framesToPop', 1); } + + // NOTE: If you are a Sentry user, and you are seeing this stack frame, + // it means the sentry.javascript SDK caught an error invoking your application code. + // This is expected behavior and NOT indicative of a bug with sentry.javascript. + throw error; }, ); }; }); } -function resolveResponse(res: Response | undefined, onFinishedResolving: () => void): void { +async function resolveResponse(res: Response | undefined, onFinishedResolving: () => void): Promise { if (res && res.body) { const responseReader = res.body.getReader(); @@ -146,25 +141,21 @@ function resolveResponse(res: Response | undefined, onFinishedResolving: () => v } } - responseReader + return responseReader .read() .then(consumeChunks) - .then(() => { - onFinishedResolving(); - }) - .catch(() => { - // noop - }); + .then(onFinishedResolving) + .catch(() => undefined); } } async function streamHandler(response: Response): Promise { // clone response for awaiting stream - let clonedResponseForResolving: Response | undefined; + let clonedResponseForResolving: Response; try { clonedResponseForResolving = response.clone(); - } catch (e) { - // noop + } catch { + return; } await resolveResponse(clonedResponseForResolving, () => { diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index 6a768627b5d2..a96fc15e35d2 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -55,6 +55,7 @@ export { setMeasurement, getActiveSpan, getRootSpan, + getTraceData, startSpan, startInactiveSpan, startSpanManual, diff --git a/packages/vue/src/router.ts b/packages/vue/src/router.ts index e54c71eb550f..8e8bf32ac172 100644 --- a/packages/vue/src/router.ts +++ b/packages/vue/src/router.ts @@ -50,18 +50,28 @@ export function instrumentVueRouter( }, startNavigationSpanFn: (context: StartSpanOptions) => void, ): void { + let isFirstPageLoad = true; + router.onError(error => captureException(error, { mechanism: { handled: false } })); router.beforeEach((to, from, next) => { - // According to docs we could use `from === VueRouter.START_LOCATION` but I couldnt get it working for Vue 2 + // According to docs we could use `from === VueRouter.START_LOCATION` but I couldn't get it working for Vue 2 // https://router.vuejs.org/api/#router-start-location // https://next.router.vuejs.org/api/#start-location + // Additionally, Nuxt does not provide the possibility to check for `from.matched.length === 0` (this is never 0). + // Therefore, a flag was added to track the page-load: isFirstPageLoad // from.name: // - Vue 2: null // - Vue 3: undefined + // - Nuxt: undefined // hence only '==' instead of '===', because `undefined == null` evaluates to `true` - const isPageLoadNavigation = from.name == null && from.matched.length === 0; + const isPageLoadNavigation = + (from.name == null && from.matched.length === 0) || (from.name === undefined && isFirstPageLoad); + + if (isFirstPageLoad) { + isFirstPageLoad = false; + } const attributes: SpanAttributes = { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue', diff --git a/packages/vue/test/router.test.ts b/packages/vue/test/router.test.ts index dc69d7ae0fd9..1da5097b11e0 100644 --- a/packages/vue/test/router.test.ts +++ b/packages/vue/test/router.test.ts @@ -114,6 +114,7 @@ describe('instrumentVueRouter()', () => { const from = testRoutes[fromKey]!; const to = testRoutes[toKey]!; + beforeEachCallback(to, testRoutes['initialPageloadRoute']!, mockNext); // fake initial pageload beforeEachCallback(to, from, mockNext); expect(mockStartSpan).toHaveBeenCalledTimes(1); @@ -127,7 +128,7 @@ describe('instrumentVueRouter()', () => { op: 'navigation', }); - expect(mockNext).toHaveBeenCalledTimes(1); + expect(mockNext).toHaveBeenCalledTimes(2); }, ); @@ -192,6 +193,7 @@ describe('instrumentVueRouter()', () => { const from = testRoutes.normalRoute1!; const to = testRoutes.namedRoute!; + beforeEachCallback(to, testRoutes['initialPageloadRoute']!, mockNext); // fake initial pageload beforeEachCallback(to, from, mockNext); // first startTx call happens when the instrumentation is initialized (for pageloads) @@ -219,6 +221,7 @@ describe('instrumentVueRouter()', () => { const from = testRoutes.normalRoute1!; const to = testRoutes.namedRoute!; + beforeEachCallback(to, testRoutes['initialPageloadRoute']!, mockNext); // fake initial pageload beforeEachCallback(to, from, mockNext); // first startTx call happens when the instrumentation is initialized (for pageloads) @@ -373,6 +376,7 @@ describe('instrumentVueRouter()', () => { expect(mockVueRouter.beforeEach).toHaveBeenCalledTimes(1); const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0]![0]!; + beforeEachCallback(testRoutes['normalRoute1']!, testRoutes['initialPageloadRoute']!, mockNext); // fake initial pageload beforeEachCallback(testRoutes['normalRoute2']!, testRoutes['normalRoute1']!, mockNext); expect(mockStartSpan).toHaveBeenCalledTimes(expectedCallsAmount); @@ -391,6 +395,7 @@ describe('instrumentVueRouter()', () => { const from = testRoutes.normalRoute1!; const to = testRoutes.namedRoute!; + beforeEachCallback(to, testRoutes['initialPageloadRoute']!, mockNext); // fake initial pageload beforeEachCallback(to, from, undefined); // first startTx call happens when the instrumentation is initialized (for pageloads) diff --git a/yarn.lock b/yarn.lock index 067cbb38765b..953f0eee5f3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4144,6 +4144,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== +"@esbuild/aix-ppc64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz#145b74d5e4a5223489cabdc238d8dad902df5259" + integrity sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ== + "@esbuild/android-arm64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz#bafb75234a5d3d1b690e7c2956a599345e84a2fd" @@ -4179,6 +4184,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== +"@esbuild/android-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz#453bbe079fc8d364d4c5545069e8260228559832" + integrity sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ== + "@esbuild/android-arm@0.15.18": version "0.15.18" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.15.18.tgz#266d40b8fdcf87962df8af05b76219bc786b4f80" @@ -4219,6 +4229,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== +"@esbuild/android-arm@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.23.0.tgz#26c806853aa4a4f7e683e519cd9d68e201ebcf99" + integrity sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g== + "@esbuild/android-x64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.19.tgz#658368ef92067866d95fb268719f98f363d13ae1" @@ -4254,6 +4269,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== +"@esbuild/android-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.23.0.tgz#1e51af9a6ac1f7143769f7ee58df5b274ed202e6" + integrity sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ== + "@esbuild/darwin-arm64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz#584c34c5991b95d4d48d333300b1a4e2ff7be276" @@ -4289,6 +4309,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== +"@esbuild/darwin-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz#d996187a606c9534173ebd78c58098a44dd7ef9e" + integrity sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow== + "@esbuild/darwin-x64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz#7751d236dfe6ce136cce343dce69f52d76b7f6cb" @@ -4324,6 +4349,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== +"@esbuild/darwin-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz#30c8f28a7ef4e32fe46501434ebe6b0912e9e86c" + integrity sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ== + "@esbuild/freebsd-arm64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz#cacd171665dd1d500f45c167d50c6b7e539d5fd2" @@ -4359,6 +4389,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== +"@esbuild/freebsd-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz#30f4fcec8167c08a6e8af9fc14b66152232e7fb4" + integrity sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw== + "@esbuild/freebsd-x64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz#0769456eee2a08b8d925d7c00b79e861cb3162e4" @@ -4394,6 +4429,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== +"@esbuild/freebsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz#1003a6668fe1f5d4439e6813e5b09a92981bc79d" + integrity sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ== + "@esbuild/linux-arm64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz#38e162ecb723862c6be1c27d6389f48960b68edb" @@ -4429,6 +4469,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== +"@esbuild/linux-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz#3b9a56abfb1410bb6c9138790f062587df3e6e3a" + integrity sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw== + "@esbuild/linux-arm@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz#1a2cd399c50040184a805174a6d89097d9d1559a" @@ -4464,6 +4509,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== +"@esbuild/linux-arm@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz#237a8548e3da2c48cd79ae339a588f03d1889aad" + integrity sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw== + "@esbuild/linux-ia32@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz#e28c25266b036ce1cabca3c30155222841dc035a" @@ -4499,6 +4549,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== +"@esbuild/linux-ia32@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz#4269cd19cb2de5de03a7ccfc8855dde3d284a238" + integrity sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA== + "@esbuild/linux-loong64@0.15.18": version "0.15.18" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz#128b76ecb9be48b60cf5cfc1c63a4f00691a3239" @@ -4544,6 +4599,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== +"@esbuild/linux-loong64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz#82b568f5658a52580827cc891cb69d2cb4f86280" + integrity sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A== + "@esbuild/linux-mips64el@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz#f5d2a0b8047ea9a5d9f592a178ea054053a70289" @@ -4579,6 +4639,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== +"@esbuild/linux-mips64el@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz#9a57386c926262ae9861c929a6023ed9d43f73e5" + integrity sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w== + "@esbuild/linux-ppc64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz#876590e3acbd9fa7f57a2c7d86f83717dbbac8c7" @@ -4614,6 +4679,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== +"@esbuild/linux-ppc64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz#f3a79fd636ba0c82285d227eb20ed8e31b4444f6" + integrity sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw== + "@esbuild/linux-riscv64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz#7f49373df463cd9f41dc34f9b2262d771688bf09" @@ -4649,6 +4719,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== +"@esbuild/linux-riscv64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz#f9d2ef8356ce6ce140f76029680558126b74c780" + integrity sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw== + "@esbuild/linux-s390x@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz#e2afd1afcaf63afe2c7d9ceacd28ec57c77f8829" @@ -4684,6 +4759,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== +"@esbuild/linux-s390x@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz#45390f12e802201f38a0229e216a6aed4351dfe8" + integrity sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg== + "@esbuild/linux-x64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz#8a0e9738b1635f0c53389e515ae83826dec22aa4" @@ -4719,6 +4799,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== +"@esbuild/linux-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz#c8409761996e3f6db29abcf9b05bee8d7d80e910" + integrity sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ== + "@esbuild/netbsd-x64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz#c29fb2453c6b7ddef9a35e2c18b37bda1ae5c462" @@ -4754,6 +4839,16 @@ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== +"@esbuild/netbsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz#ba70db0114380d5f6cfb9003f1d378ce989cd65c" + integrity sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw== + +"@esbuild/openbsd-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz#72fc55f0b189f7a882e3cf23f332370d69dfd5db" + integrity sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ== + "@esbuild/openbsd-x64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz#95e75a391403cb10297280d524d66ce04c920691" @@ -4789,6 +4884,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== +"@esbuild/openbsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz#b6ae7a0911c18fe30da3db1d6d17a497a550e5d8" + integrity sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg== + "@esbuild/sunos-x64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz#722eaf057b83c2575937d3ffe5aeb16540da7273" @@ -4824,6 +4924,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== +"@esbuild/sunos-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz#58f0d5e55b9b21a086bfafaa29f62a3eb3470ad8" + integrity sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA== + "@esbuild/win32-arm64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz#9aa9dc074399288bdcdd283443e9aeb6b9552b6f" @@ -4859,6 +4964,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== +"@esbuild/win32-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz#b858b2432edfad62e945d5c7c9e5ddd0f528ca6d" + integrity sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ== + "@esbuild/win32-ia32@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz#95ad43c62ad62485e210f6299c7b2571e48d2b03" @@ -4894,6 +5004,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== +"@esbuild/win32-ia32@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz#167ef6ca22a476c6c0c014a58b4f43ae4b80dec7" + integrity sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA== + "@esbuild/win32-x64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz#8cfaf2ff603e9aabb910e9c0558c26cf32744061" @@ -4929,6 +5044,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== +"@esbuild/win32-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz#db44a6a08520b5f25bbe409f34a59f2d4bcc7ced" + integrity sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g== + "@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -6437,6 +6557,21 @@ unimport "^3.7.2" untyped "^1.4.2" +"@nuxt/module-builder@0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@nuxt/module-builder/-/module-builder-0.8.1.tgz#f4d7b3edf949416ded0b37e4c137a865abc4efdb" + integrity sha512-pWIRF2x6zx63WEh3z7zM37CTfwhsWz21QnFWOeLacqDIBF1G92cRxF5BiS8mn7qfybFop8HRyZGzGDQeCsI20A== + dependencies: + citty "^0.1.6" + consola "^3.2.3" + defu "^6.1.4" + magic-regexp "^0.8.0" + mlly "^1.7.1" + pathe "^1.1.2" + pkg-types "^1.1.1" + tsconfck "^3.1.1" + unbuild "^2.0.0" + "@nuxt/schema@3.12.2", "@nuxt/schema@^3.11.2": version "3.12.2" resolved "https://registry.yarnpkg.com/@nuxt/schema/-/schema-3.12.2.tgz#dc2c3bced5a6965075dabfb372dd2f77bb3b33c6" @@ -7565,7 +7700,7 @@ dependencies: web-streams-polyfill "^3.1.1" -"@rollup/plugin-alias@^5.1.0": +"@rollup/plugin-alias@^5.0.0", "@rollup/plugin-alias@^5.1.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@rollup/plugin-alias/-/plugin-alias-5.1.0.tgz#99a94accc4ff9a3483be5baeedd5d7da3b597e93" integrity sha512-lpA3RZ9PdIG7qqhEfv79tBffNaoDuukFDrmhLqg9ifv99u/ehn+lOg30x2zmhf8AQqQUZaMk/B9fZraQ6/acDQ== @@ -7584,6 +7719,18 @@ is-reference "1.2.1" magic-string "^0.30.3" +"@rollup/plugin-commonjs@^25.0.4": + version "25.0.8" + resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.8.tgz#c77e608ab112a666b7f2a6bea625c73224f7dd34" + integrity sha512-ZEZWTK5n6Qde0to4vS9Mr5x/0UZoqCxPVR9KRUjU4kA2sO7GEUn1fop0DAwpO6z0Nw/kJON9bDmSxdWxO/TT1A== + dependencies: + "@rollup/pluginutils" "^5.0.1" + commondir "^1.0.1" + estree-walker "^2.0.2" + glob "^8.0.3" + is-reference "1.2.1" + magic-string "^0.30.3" + "@rollup/plugin-commonjs@^25.0.7": version "25.0.7" resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz#145cec7589ad952171aeb6a585bbeabd0fd3b4cf" @@ -7619,7 +7766,7 @@ dependencies: "@rollup/pluginutils" "^3.0.8" -"@rollup/plugin-json@^6.1.0": +"@rollup/plugin-json@^6.0.0", "@rollup/plugin-json@^6.1.0": version "6.1.0" resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-6.1.0.tgz#fbe784e29682e9bb6dee28ea75a1a83702e7b805" integrity sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA== @@ -7638,7 +7785,7 @@ is-module "^1.0.0" resolve "^1.19.0" -"@rollup/plugin-node-resolve@^15.2.3": +"@rollup/plugin-node-resolve@^15.2.1", "@rollup/plugin-node-resolve@^15.2.3": version "15.2.3" resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz#e5e0b059bd85ca57489492f295ce88c2d4b0daf9" integrity sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ== @@ -7650,18 +7797,18 @@ is-module "^1.0.0" resolve "^1.22.1" -"@rollup/plugin-replace@^5.0.5": - version "5.0.5" - resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz#33d5653dce6d03cb24ef98bef7f6d25b57faefdf" - integrity sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ== +"@rollup/plugin-replace@^5.0.2", "@rollup/plugin-replace@^5.0.7": + version "5.0.7" + resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-5.0.7.tgz#150c9ee9db8031d9e4580a61a0edeaaed3d37687" + integrity sha512-PqxSfuorkHz/SPpyngLyg5GCEkOcee9M1bkxiVDr41Pd61mqP1PLOoDPbpl44SB2mQGKwV/In74gqQmGITOhEQ== dependencies: "@rollup/pluginutils" "^5.0.1" magic-string "^0.30.3" -"@rollup/plugin-replace@^5.0.7": - version "5.0.7" - resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-5.0.7.tgz#150c9ee9db8031d9e4580a61a0edeaaed3d37687" - integrity sha512-PqxSfuorkHz/SPpyngLyg5GCEkOcee9M1bkxiVDr41Pd61mqP1PLOoDPbpl44SB2mQGKwV/In74gqQmGITOhEQ== +"@rollup/plugin-replace@^5.0.5": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz#33d5653dce6d03cb24ef98bef7f6d25b57faefdf" + integrity sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ== dependencies: "@rollup/pluginutils" "^5.0.1" magic-string "^0.30.3" @@ -7725,7 +7872,7 @@ estree-walker "^2.0.2" picomatch "^2.3.1" -"@rollup/pluginutils@^5.0.2", "@rollup/pluginutils@^5.0.4", "@rollup/pluginutils@^5.0.5", "@rollup/pluginutils@^5.1.0": +"@rollup/pluginutils@^5.0.2", "@rollup/pluginutils@^5.0.3", "@rollup/pluginutils@^5.0.4", "@rollup/pluginutils@^5.0.5", "@rollup/pluginutils@^5.1.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.0.tgz#7e53eddc8c7f483a4ad0b94afb1f7f5fd3c771e0" integrity sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g== @@ -11265,16 +11412,16 @@ acorn@^8.10.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== +acorn@^8.12.1, acorn@^8.8.0: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== + acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.0, acorn@^8.7.1: version "8.8.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== -acorn@^8.8.0: - version "8.12.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" - integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== - acorn@^8.8.1, acorn@^8.8.2: version "8.8.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" @@ -13937,7 +14084,7 @@ ci-info@^4.0.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.0.0.tgz#65466f8b280fc019b9f50a5388115d17a63a44f2" integrity sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg== -citty@^0.1.5, citty@^0.1.6: +citty@^0.1.2, citty@^0.1.5, citty@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/citty/-/citty-0.1.6.tgz#0f7904da1ed4625e1a9ea7e0fa780981aab7c5e4" integrity sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ== @@ -14999,6 +15146,42 @@ cssnano-preset-default@^7.0.3: postcss-svgo "^7.0.1" postcss-unique-selectors "^7.0.1" +cssnano-preset-default@^7.0.4: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-7.0.4.tgz#9cfcd25f85bfedc84367b881dad56b75a0f976b5" + integrity sha512-jQ6zY9GAomQX7/YNLibMEsRZguqMUGuupXcEk2zZ+p3GUxwCAsobqPYE62VrJ9qZ0l9ltrv2rgjwZPBIFIjYtw== + dependencies: + browserslist "^4.23.1" + css-declaration-sorter "^7.2.0" + cssnano-utils "^5.0.0" + postcss-calc "^10.0.0" + postcss-colormin "^7.0.1" + postcss-convert-values "^7.0.2" + postcss-discard-comments "^7.0.1" + postcss-discard-duplicates "^7.0.0" + postcss-discard-empty "^7.0.0" + postcss-discard-overridden "^7.0.0" + postcss-merge-longhand "^7.0.2" + postcss-merge-rules "^7.0.2" + postcss-minify-font-values "^7.0.0" + postcss-minify-gradients "^7.0.0" + postcss-minify-params "^7.0.1" + postcss-minify-selectors "^7.0.2" + postcss-normalize-charset "^7.0.0" + postcss-normalize-display-values "^7.0.0" + postcss-normalize-positions "^7.0.0" + postcss-normalize-repeat-style "^7.0.0" + postcss-normalize-string "^7.0.0" + postcss-normalize-timing-functions "^7.0.0" + postcss-normalize-unicode "^7.0.1" + postcss-normalize-url "^7.0.0" + postcss-normalize-whitespace "^7.0.0" + postcss-ordered-values "^7.0.1" + postcss-reduce-initial "^7.0.1" + postcss-reduce-transforms "^7.0.0" + postcss-svgo "^7.0.1" + postcss-unique-selectors "^7.0.1" + cssnano-utils@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-5.0.0.tgz#b53a0343dd5d21012911882db6ae7d2eae0e3687" @@ -15012,6 +15195,14 @@ cssnano@^7.0.2: cssnano-preset-default "^7.0.3" lilconfig "^3.1.2" +cssnano@^7.0.4: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-7.0.4.tgz#13a4fb4dd14f3b1ee0cd51e6404ae4656f8ad9a0" + integrity sha512-rQgpZra72iFjiheNreXn77q1haS2GEy69zCMbu4cpXCFPMQF+D4Ik5V7ktMzUF/sA7xCIgcqHwGPnCD+0a1vHg== + dependencies: + cssnano-preset-default "^7.0.4" + lilconfig "^3.1.2" + csso@^5.0.5: version "5.0.5" resolved "https://registry.yarnpkg.com/csso/-/csso-5.0.5.tgz#f9b7fe6cc6ac0b7d90781bb16d5e9874303e2ca6" @@ -17371,6 +17562,36 @@ esbuild@^0.21.3, esbuild@^0.21.5: "@esbuild/win32-ia32" "0.21.5" "@esbuild/win32-x64" "0.21.5" +esbuild@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.23.0.tgz#de06002d48424d9fdb7eb52dbe8e95927f852599" + integrity sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA== + optionalDependencies: + "@esbuild/aix-ppc64" "0.23.0" + "@esbuild/android-arm" "0.23.0" + "@esbuild/android-arm64" "0.23.0" + "@esbuild/android-x64" "0.23.0" + "@esbuild/darwin-arm64" "0.23.0" + "@esbuild/darwin-x64" "0.23.0" + "@esbuild/freebsd-arm64" "0.23.0" + "@esbuild/freebsd-x64" "0.23.0" + "@esbuild/linux-arm" "0.23.0" + "@esbuild/linux-arm64" "0.23.0" + "@esbuild/linux-ia32" "0.23.0" + "@esbuild/linux-loong64" "0.23.0" + "@esbuild/linux-mips64el" "0.23.0" + "@esbuild/linux-ppc64" "0.23.0" + "@esbuild/linux-riscv64" "0.23.0" + "@esbuild/linux-s390x" "0.23.0" + "@esbuild/linux-x64" "0.23.0" + "@esbuild/netbsd-x64" "0.23.0" + "@esbuild/openbsd-arm64" "0.23.0" + "@esbuild/openbsd-x64" "0.23.0" + "@esbuild/sunos-x64" "0.23.0" + "@esbuild/win32-arm64" "0.23.0" + "@esbuild/win32-ia32" "0.23.0" + "@esbuild/win32-x64" "0.23.0" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -19324,7 +19545,7 @@ globby@11, globby@11.1.0, globby@^11.0.3, globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" -globby@^13.1.1: +globby@^13.1.1, globby@^13.2.2: version "13.2.2" resolved "https://registry.yarnpkg.com/globby/-/globby-13.2.2.tgz#63b90b1bf68619c2135475cbd4e71e66aa090592" integrity sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w== @@ -21970,16 +22191,16 @@ jest@^27.5.1: import-local "^3.0.2" jest-cli "^27.5.1" +jiti@^1.19.3, jiti@^1.21.6: + version "1.21.6" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" + integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== + jiti@^1.21.0: version "1.21.0" resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== -jiti@^1.21.6: - version "1.21.6" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" - integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== - js-cleanup@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/js-cleanup/-/js-cleanup-1.2.0.tgz#8dbc65954b1d38b255f1e8cf02cd17b3f7a053f9" @@ -23171,6 +23392,19 @@ madge@7.0.0: ts-graphviz "^1.8.1" walkdir "^0.4.1" +magic-regexp@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/magic-regexp/-/magic-regexp-0.8.0.tgz#c67de16456522a83672c22aa408b774facfd882e" + integrity sha512-lOSLWdE156csDYwCTIGiAymOLN7Epu/TU5e/oAnISZfU6qP+pgjkE+xbVjVn3yLPKN8n1G2yIAYTAM5KRk6/ow== + dependencies: + estree-walker "^3.0.3" + magic-string "^0.30.8" + mlly "^1.6.1" + regexp-tree "^0.1.27" + type-level-regexp "~0.1.17" + ufo "^1.4.0" + unplugin "^1.8.3" + magic-string-ast@^0.6.0: version "0.6.1" resolved "https://registry.yarnpkg.com/magic-string-ast/-/magic-string-ast-0.6.1.tgz#c1e5d78b20ec920265567446181f6e5c521e8217" @@ -24113,10 +24347,10 @@ mini-css-extract-plugin@2.6.1, mini-css-extract-plugin@^2.5.2: dependencies: schema-utils "^4.0.0" -miniflare@3.20240718.0, miniflare@^3.20240718.0: - version "3.20240718.0" - resolved "https://registry.yarnpkg.com/miniflare/-/miniflare-3.20240718.0.tgz#41561c6620b2b15803f5b3d2e903ed3af40f3b0b" - integrity sha512-TKgSeyqPBeT8TBLxbDJOKPWlq/wydoJRHjAyDdgxbw59N6wbP8JucK6AU1vXCfu21eKhrEin77ssXOpbfekzPA== +miniflare@3.20240718.1: + version "3.20240718.1" + resolved "https://registry.yarnpkg.com/miniflare/-/miniflare-3.20240718.1.tgz#26ccb95be087cd99cd478dbf2e3a3d40f231bf45" + integrity sha512-mn3MjGnpgYvarCRTfz4TQyVyY8yW0zz7f8LOAPVai78IGC/lcVcyskZcuIr7Zovb2i+IERmmsJAiEPeZHIIKbA== dependencies: "@cspotcode/source-map-support" "0.8.1" acorn "^8.8.0" @@ -24367,6 +24601,25 @@ mkdirp@~3.0.0: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== +mkdist@^1.3.0: + version "1.5.4" + resolved "https://registry.yarnpkg.com/mkdist/-/mkdist-1.5.4.tgz#c2343fab3297e49896013563fb3b0113a07b65da" + integrity sha512-GEmKYJG5K1YGFIq3t0K3iihZ8FTgXphLf/4UjbmpXIAtBFn4lEjXk3pXNTSfy7EtcEXhp2Nn1vzw5pIus6RY3g== + dependencies: + autoprefixer "^10.4.19" + citty "^0.1.6" + cssnano "^7.0.4" + defu "^6.1.4" + esbuild "^0.23.0" + fast-glob "^3.3.2" + jiti "^1.21.6" + mlly "^1.7.1" + pathe "^1.1.2" + pkg-types "^1.1.3" + postcss "^8.4.39" + postcss-nested "^6.0.1" + semver "^7.6.2" + mktemp@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/mktemp/-/mktemp-0.4.0.tgz#6d0515611c8a8c84e484aa2000129b98e981ff0b" @@ -24382,7 +24635,7 @@ mlly@^1.2.0, mlly@^1.4.2: pkg-types "^1.0.3" ufo "^1.3.2" -mlly@^1.3.0, mlly@^1.6.1, mlly@^1.7.0, mlly@^1.7.1: +mlly@^1.3.0, mlly@^1.4.0, mlly@^1.6.1, mlly@^1.7.0, mlly@^1.7.1: version "1.7.1" resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.1.tgz#e0336429bb0731b6a8e887b438cbdae522c8f32f" integrity sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA== @@ -26880,6 +27133,15 @@ pkg-types@^1.1.1: mlly "^1.7.0" pathe "^1.1.2" +pkg-types@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.1.3.tgz#161bb1242b21daf7795036803f28e30222e476e3" + integrity sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA== + dependencies: + confbox "^0.1.7" + mlly "^1.7.1" + pathe "^1.1.2" + pkg-up@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f" @@ -26993,6 +27255,14 @@ postcss-convert-values@^7.0.1: browserslist "^4.23.1" postcss-value-parser "^4.2.0" +postcss-convert-values@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-7.0.2.tgz#8a33265f5f1decfc93328e2a23e03e8491a3d9ae" + integrity sha512-MuZIF6HJ4izko07Q0TgW6pClalI4al6wHRNPkFzqQdwAwG7hPn0lA58VZdxyb2Vl5AYjJ1piO+jgF9EnTjQwQQ== + dependencies: + browserslist "^4.23.1" + postcss-value-parser "^4.2.0" + postcss-custom-media@^8.0.2: version "8.0.2" resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz#c8f9637edf45fef761b014c024cee013f80529ea" @@ -27209,6 +27479,13 @@ postcss-modules-values@^4.0.0: dependencies: icss-utils "^5.0.0" +postcss-nested@^6.0.1: + version "6.2.0" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.2.0.tgz#4c2d22ab5f20b9cb61e2c5c5915950784d068131" + integrity sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ== + dependencies: + postcss-selector-parser "^6.1.1" + postcss-nesting@^10.1.10, postcss-nesting@^10.2.0: version "10.2.0" resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-10.2.0.tgz#0b12ce0db8edfd2d8ae0aaf86427370b898890be" @@ -27487,6 +27764,14 @@ postcss-selector-parser@^6.0.9: cssesc "^3.0.0" util-deprecate "^1.0.2" +postcss-selector-parser@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz#5be94b277b8955904476a2400260002ce6c56e38" + integrity sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + postcss-svgo@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-7.0.1.tgz#2b63571d8e9568384df334bac9917baff4d23f58" @@ -27585,6 +27870,15 @@ postcss@^8.4.32: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.4.39: + version "8.4.40" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.40.tgz#eb81f2a4dd7668ed869a6db25999e02e9ad909d8" + integrity sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.1" + source-map-js "^1.2.0" + postgres-array@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" @@ -28607,6 +28901,11 @@ regexp-clone@1.0.0, regexp-clone@^1.0.0: resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-1.0.0.tgz#222db967623277056260b992626354a04ce9bf63" integrity sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw== +regexp-tree@^0.1.27: + version "0.1.27" + resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd" + integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== + regexp.prototype.flags@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" @@ -29181,6 +29480,15 @@ rollup-plugin-cleanup@^3.2.1: js-cleanup "^1.2.0" rollup-pluginutils "^2.8.2" +rollup-plugin-dts@^6.0.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/rollup-plugin-dts/-/rollup-plugin-dts-6.1.1.tgz#46b33f4d1d7f4e66f1171ced9b282ac11a15a254" + integrity sha512-aSHRcJ6KG2IHIioYlvAOcEq6U99sVtqDDKVhnwt70rW6tsz3tv5OSjEiWcgzfsHdLyGXZ/3b/7b/+Za3Y6r1XA== + dependencies: + magic-string "^0.30.10" + optionalDependencies: + "@babel/code-frame" "^7.24.2" + rollup-plugin-dts@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/rollup-plugin-dts/-/rollup-plugin-dts-6.1.0.tgz#56e9c5548dac717213c6a4aa9df523faf04f75ae" @@ -29246,7 +29554,7 @@ rollup-pluginutils@^2.8.1, rollup-pluginutils@^2.8.2: dependencies: estree-walker "^0.6.1" -rollup@3.29.4, rollup@^3.27.1: +rollup@3.29.4, rollup@^3.27.1, rollup@^3.28.1: version "3.29.4" resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981" integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw== @@ -31793,6 +32101,11 @@ tsconfck@^3.0.0: resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.0.0.tgz#b469f1ced12973bbec3209a55ed8de3bb04223c9" integrity sha512-w3wnsIrJNi7avf4Zb0VjOoodoO0woEqGgZGQm+LHH9przdUI+XDKsWAXwxHA1DaRTjeuZNcregSzr7RaA8zG9A== +tsconfck@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.1.tgz#c7284913262c293b43b905b8b034f524de4a3162" + integrity sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ== + tsconfig-paths@^3.9.0: version "3.9.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" @@ -31960,6 +32273,11 @@ type-is@^1.6.4, type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +type-level-regexp@~0.1.17: + version "0.1.17" + resolved "https://registry.yarnpkg.com/type-level-regexp/-/type-level-regexp-0.1.17.tgz#ec1bf7dd65b85201f9863031d6f023bdefc2410f" + integrity sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg== + typed-assert@^1.0.8: version "1.0.9" resolved "https://registry.yarnpkg.com/typed-assert/-/typed-assert-1.0.9.tgz#8af9d4f93432c4970ec717e3006f33f135b06213" @@ -32091,6 +32409,36 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +unbuild@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unbuild/-/unbuild-2.0.0.tgz#9e2117e83ce5d93bae0c9ee056c3f6c241ea4fbc" + integrity sha512-JWCUYx3Oxdzvw2J9kTAp+DKE8df/BnH/JTSj6JyA4SH40ECdFu7FoJJcrm8G92B7TjofQ6GZGjJs50TRxoH6Wg== + dependencies: + "@rollup/plugin-alias" "^5.0.0" + "@rollup/plugin-commonjs" "^25.0.4" + "@rollup/plugin-json" "^6.0.0" + "@rollup/plugin-node-resolve" "^15.2.1" + "@rollup/plugin-replace" "^5.0.2" + "@rollup/pluginutils" "^5.0.3" + chalk "^5.3.0" + citty "^0.1.2" + consola "^3.2.3" + defu "^6.1.2" + esbuild "^0.19.2" + globby "^13.2.2" + hookable "^5.5.3" + jiti "^1.19.3" + magic-string "^0.30.3" + mkdist "^1.3.0" + mlly "^1.4.0" + pathe "^1.1.1" + pkg-types "^1.0.3" + pretty-bytes "^6.1.1" + rollup "^3.28.1" + rollup-plugin-dts "^6.0.0" + scule "^1.0.0" + untyped "^1.4.0" + uncrypto@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/uncrypto/-/uncrypto-0.1.3.tgz#e1288d609226f2d02d8d69ee861fa20d8348ef2b" @@ -32460,6 +32808,16 @@ unplugin@^1.10.0, unplugin@^1.10.1, unplugin@^1.3.1, unplugin@^1.5.0: webpack-sources "^3.2.3" webpack-virtual-modules "^0.6.1" +unplugin@^1.8.3: + version "1.12.0" + resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.12.0.tgz#a11d3eb565602190748b1f95ecc8590b0f7dcbb4" + integrity sha512-KeczzHl2sATPQUx1gzo+EnUkmN4VmGBYRRVOZSGvGITE9rGHRDGqft6ONceP3vgXcyJ2XjX5axG5jMWUwNCYLw== + dependencies: + acorn "^8.12.1" + chokidar "^3.6.0" + webpack-sources "^3.2.3" + webpack-virtual-modules "^0.6.2" + unset-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" @@ -32505,7 +32863,7 @@ untun@^0.1.3: consola "^3.2.3" pathe "^1.1.1" -untyped@^1.4.2: +untyped@^1.4.0, untyped@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/untyped/-/untyped-1.4.2.tgz#7945ea53357635434284e6112fd1afe84dd5dcab" integrity sha512-nC5q0DnPEPVURPhfPQLahhSTnemVtPzdx7ofiRxXpOB2SYnb3MfdU3DVGyJdS8Lx+tBWeAePO8BfU/3EgksM7Q== @@ -33410,6 +33768,11 @@ webpack-virtual-modules@^0.6.1: resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.1.tgz#ac6fdb9c5adb8caecd82ec241c9631b7a3681b6f" integrity sha512-poXpCylU7ExuvZK8z+On3kX+S8o/2dQ/SVYueKA0D4WEMXROXgY8Ez50/bQEUmvoSMMrWcrJqCHuhAbsiwg7Dg== +webpack-virtual-modules@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8" + integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ== + webpack@5.76.1: version "5.76.1" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.1.tgz#7773de017e988bccb0f13c7d75ec245f377d295c" @@ -33755,10 +34118,10 @@ workerpool@^6.4.0: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.4.0.tgz#f8d5cfb45fde32fa3b7af72ad617c3369567a462" integrity sha512-i3KR1mQMNwY2wx20ozq2EjISGtQWDIfV56We+yGJ5yDs8jTwQiLLaqHlkBHITlCuJnYlVRmXegxFxZg7gqI++A== -wrangler@^3.65.1: - version "3.65.1" - resolved "https://registry.yarnpkg.com/wrangler/-/wrangler-3.65.1.tgz#493bd92b504f9f056cd57bbe2d430797600c914b" - integrity sha512-Z5NyrbpGMQCpim/6VnI1im0/Weh5+CU1sdep1JbfFxHjn/Jt9K+MeUq+kCns5ubkkdRx2EYsusB/JKyX2JdJ4w== +wrangler@^3.67.1: + version "3.67.1" + resolved "https://registry.yarnpkg.com/wrangler/-/wrangler-3.67.1.tgz#c9bb344b70c8c2106ad33f03beaa063dd5b49526" + integrity sha512-lLVJxq/OZMfntvZ79WQJNC1OKfxOCs6PLfogqDBuPFEQ3L/Mwqvd9IZ0bB8ahrwUN/K3lSdDPXynk9HfcGZxVw== dependencies: "@cloudflare/kv-asset-handler" "0.3.4" "@esbuild-plugins/node-globals-polyfill" "^0.2.3" @@ -33767,7 +34130,7 @@ wrangler@^3.65.1: chokidar "^3.5.3" date-fns "^3.6.0" esbuild "0.17.19" - miniflare "3.20240718.0" + miniflare "3.20240718.1" nanoid "^3.3.3" path-to-regexp "^6.2.0" resolve "^1.22.8" @@ -33775,6 +34138,7 @@ wrangler@^3.65.1: selfsigned "^2.0.1" source-map "^0.6.1" unenv "npm:unenv-nightly@1.10.0-1717606461.a117952" + workerd "1.20240718.0" xxhash-wasm "^1.0.1" optionalDependencies: fsevents "~2.3.2"