diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9ef18fe35116..dd827c293a61 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -436,8 +436,6 @@ jobs: env: DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} - name: Run tests - env: - NODE_VERSION: 16 run: yarn test-ci-browser - name: Compute test coverage uses: codecov/codecov-action@v4 @@ -1005,7 +1003,9 @@ jobs: 'create-remix-app-express-vite-dev', 'debug-id-sourcemaps', 'node-express-esm-loader', + 'node-express-esm-preload', 'node-express-esm-without-loader', + 'node-express-cjs-preload', 'nextjs-app-dir', 'nextjs-14', 'nextjs-15', diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 25004eee8438..5f58292646df 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -159,7 +159,8 @@ jobs: strategy: fail-fast: false matrix: - scenario: [ember-release, embroider-optimized, ember-4.0] + # scenario: [ember-release, embroider-optimized, ember-4.0] + scenario: [ember-4.0] steps: - name: 'Check out current commit' uses: actions/checkout@v4 diff --git a/.size-limit.js b/.size-limit.js index 1747b93aea21..0d8af3d8d7d5 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -57,6 +57,20 @@ module.exports = [ gzip: true, limit: '87 KB', }, + { + name: '@sentry/browser (incl. Tracing, Replay, Feedback, metrics)', + path: 'packages/browser/build/npm/esm/index.js', + import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'feedbackIntegration', 'metrics'), + gzip: true, + limit: '100 KB', + }, + { + name: '@sentry/browser (incl. metrics)', + path: 'packages/browser/build/npm/esm/index.js', + import: createImport('init', 'metrics'), + gzip: true, + limit: '40 KB', + }, { name: '@sentry/browser (incl. Feedback)', path: 'packages/browser/build/npm/esm/index.js', @@ -83,6 +97,7 @@ module.exports = [ name: '@sentry/react', path: 'packages/react/build/esm/index.js', import: createImport('init', 'ErrorBoundary'), + ignore: ['react/jsx-runtime'], gzip: true, limit: '27 KB', }, @@ -90,6 +105,7 @@ module.exports = [ name: '@sentry/react (incl. Tracing)', path: 'packages/react/build/esm/index.js', import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'), + ignore: ['react/jsx-runtime'], gzip: true, limit: '37 KB', }, diff --git a/CHANGELOG.md b/CHANGELOG.md index f7659733d57c..4b9edfc5bd99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,59 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 8.5.0 + +### Important Changes + +- **feat(react): Add React 19 to peer deps (#12207)** + +This release adds support for React 19 in the `@sentry/react` SDK package. + +- **feat(node): Add `@sentry/node/preload` hook (#12213)** + +This release adds a new way to initialize `@sentry/node`, which allows you to use the SDK with performance +instrumentation even if you cannot call `Sentry.init()` at the very start of your app. + +First, run the SDK like this: + +```bash +node --require @sentry/node/preload ./app.js +``` + +Now, you can initialize and import the rest of the SDK later or asynchronously: + +```js +const express = require('express'); +const Sentry = require('@sentry/node'); + +const dsn = await getSentryDsn(); +Sentry.init({ dsn }); +``` + +For more details, head over to the +[PR Description of the new feature](https://github.com/getsentry/sentry-javascript/pull/12213). Our docs will be updated +soon with a new guide. + +### Other Changes + +- feat(browser): Do not include metrics in base CDN bundle (#12230) +- feat(core): Add `startNewTrace` API (#12138) +- feat(core): Allow to pass custom scope to `captureFeedback()` (#12216) +- feat(core): Only allow `SerializedSession` in session envelope items (#11979) +- feat(nextjs): Use Vercel's `waitUntil` to defer freezing of Vercel Lambdas (#12133) +- feat(node): Ensure manual OTEL setup works (#12214) +- fix(aws-serverless): Avoid minifying `Module._resolveFilename` in Lambda layer bundle (#12232) +- fix(aws-serverless): Ensure lambda layer uses default export from `ImportInTheMiddle` (#12233) +- fix(browser): Improve browser extension error message check (#12146) +- fix(browser): Remove optional chaining in INP code (#12196) +- fix(nextjs): Don't report React postpone errors (#12194) +- fix(nextjs): Use global scope for generic event filters (#12205) +- fix(node): Add origin to redis span (#12201) +- fix(node): Change import of `@prisma/instrumentation` to use default import (#12185) +- fix(node): Only import `inspector` asynchronously (#12231) +- fix(replay): Update matcher for hydration error detection to new React docs (#12209) +- ref(profiling-node): Add warning when using non-LTS node (#12211) + ## 8.4.0 ### Important Changes diff --git a/dev-packages/browser-integration-tests/suites/metrics/init.js b/dev-packages/browser-integration-tests/suites/metrics/metricsEvent/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/metrics/init.js rename to dev-packages/browser-integration-tests/suites/metrics/metricsEvent/init.js diff --git a/dev-packages/browser-integration-tests/suites/metrics/test.ts b/dev-packages/browser-integration-tests/suites/metrics/metricsEvent/test.ts similarity index 73% rename from dev-packages/browser-integration-tests/suites/metrics/test.ts rename to dev-packages/browser-integration-tests/suites/metrics/metricsEvent/test.ts index 5c6ff8bb13a4..05a6d238be93 100644 --- a/dev-packages/browser-integration-tests/suites/metrics/test.ts +++ b/dev-packages/browser-integration-tests/suites/metrics/metricsEvent/test.ts @@ -1,9 +1,17 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, properEnvelopeRequestParser } from '../../utils/helpers'; +import { sentryTest } from '../../../utils/fixtures'; +import { + getFirstSentryEnvelopeRequest, + properEnvelopeRequestParser, + shouldSkipMetricsTest, +} from '../../../utils/helpers'; sentryTest('collects metrics', async ({ getLocalTestUrl, page }) => { + if (shouldSkipMetricsTest()) { + sentryTest.skip(); + } + const url = await getLocalTestUrl({ testDir: __dirname }); const statsdBuffer = await getFirstSentryEnvelopeRequest(page, url, properEnvelopeRequestParser); diff --git a/dev-packages/browser-integration-tests/suites/metrics/metricsShim/init.js b/dev-packages/browser-integration-tests/suites/metrics/metricsShim/init.js new file mode 100644 index 000000000000..93c639cbdff9 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/metrics/metricsShim/init.js @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', +}); + +// This should not fail +Sentry.metrics.increment('increment'); +Sentry.metrics.distribution('distribution', 42); +Sentry.metrics.gauge('gauge', 5); +Sentry.metrics.set('set', 'nope'); diff --git a/dev-packages/browser-integration-tests/suites/metrics/metricsShim/test.ts b/dev-packages/browser-integration-tests/suites/metrics/metricsShim/test.ts new file mode 100644 index 000000000000..ba86f0a991f5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/metrics/metricsShim/test.ts @@ -0,0 +1,36 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../utils/fixtures'; +import { shouldSkipMetricsTest } from '../../../utils/helpers'; + +sentryTest('exports shim metrics integration for non-tracing bundles', async ({ getLocalTestPath, page }) => { + // Skip in tracing tests + if (!shouldSkipMetricsTest()) { + sentryTest.skip(); + } + + const consoleMessages: string[] = []; + page.on('console', msg => consoleMessages.push(msg.text())); + + let requestCount = 0; + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + requestCount++; + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + + expect(requestCount).toBe(0); + expect(consoleMessages).toEqual([ + 'You are using metrics even though this bundle does not include tracing.', + 'You are using metrics even though this bundle does not include tracing.', + 'You are using metrics even though this bundle does not include tracing.', + 'You are using metrics even though this bundle does not include tracing.', + ]); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/subject.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/subject.js new file mode 100644 index 000000000000..5b28df9da5e8 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/subject.js @@ -0,0 +1,15 @@ +const newTraceBtn = document.getElementById('newTrace'); +newTraceBtn.addEventListener('click', async () => { + Sentry.startNewTrace(() => { + Sentry.startSpan({ op: 'ui.interaction.click', name: 'new-trace' }, async () => { + await fetch('http://example.com'); + }); + }); +}); + +const oldTraceBtn = document.getElementById('oldTrace'); +oldTraceBtn.addEventListener('click', async () => { + Sentry.startSpan({ op: 'ui.interaction.click', name: 'old-trace' }, async () => { + await fetch('http://example.com'); + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/template.html b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/template.html new file mode 100644 index 000000000000..7d3c25bf7b84 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/template.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/test.ts new file mode 100644 index 000000000000..3ddca4787aee --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/test.ts @@ -0,0 +1,106 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import type { EventAndTraceHeader } from '../../../../utils/helpers'; +import { + eventAndTraceHeaderRequestParser, + getFirstSentryEnvelopeRequest, + getMultipleSentryEnvelopeRequests, + shouldSkipTracingTest, +} from '../../../../utils/helpers'; + +sentryTest( + 'creates a new trace if `startNewTrace` is called and leaves old trace valid outside the callback', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.route('http://example.com/**', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); + + const [pageloadEvent, pageloadTraceHeaders] = await getFirstSentryEnvelopeRequest( + page, + url, + eventAndTraceHeaderRequestParser, + ); + + const pageloadTraceContext = pageloadEvent.contexts?.trace; + + expect(pageloadEvent.type).toEqual('transaction'); + + expect(pageloadTraceContext).toMatchObject({ + op: 'pageload', + trace_id: expect.stringMatching(/^[0-9a-f]{32}$/), + span_id: expect.stringMatching(/^[0-9a-f]{16}$/), + }); + expect(pageloadTraceContext).not.toHaveProperty('parent_span_id'); + + expect(pageloadTraceHeaders).toEqual({ + environment: 'production', + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: pageloadTraceContext?.trace_id, + }); + + const transactionPromises = getMultipleSentryEnvelopeRequests( + page, + 2, + { envelopeType: 'transaction' }, + eventAndTraceHeaderRequestParser, + ); + + await page.locator('#newTrace').click(); + await page.locator('#oldTrace').click(); + + const [txnEvent1, txnEvent2] = await transactionPromises; + + const [newTraceTransactionEvent, newTraceTransactionTraceHeaders] = + txnEvent1[0].transaction === 'new-trace' ? txnEvent1 : txnEvent2; + const [oldTraceTransactionEvent, oldTraceTransactionTraceHeaders] = + txnEvent1[0].transaction === 'old-trace' ? txnEvent1 : txnEvent2; + + const newTraceTransactionTraceContext = newTraceTransactionEvent.contexts?.trace; + expect(newTraceTransactionTraceContext).toMatchObject({ + op: 'ui.interaction.click', + trace_id: expect.stringMatching(/^[0-9a-f]{32}$/), + span_id: expect.stringMatching(/^[0-9a-f]{16}$/), + }); + + expect(newTraceTransactionTraceHeaders).toEqual({ + environment: 'production', + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: newTraceTransactionTraceContext?.trace_id, + transaction: 'new-trace', + }); + + const oldTraceTransactionEventTraceContext = oldTraceTransactionEvent.contexts?.trace; + expect(oldTraceTransactionEventTraceContext).toMatchObject({ + op: 'ui.interaction.click', + trace_id: expect.stringMatching(/^[0-9a-f]{32}$/), + span_id: expect.stringMatching(/^[0-9a-f]{16}$/), + }); + + expect(oldTraceTransactionTraceHeaders).toEqual({ + environment: 'production', + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: oldTraceTransactionTraceHeaders?.trace_id, + // transaction: 'old-trace', <-- this is not in the DSC because the DSC is continued from the pageload transaction + // which does not have a `transaction` field because its source is URL. + }); + + expect(oldTraceTransactionEventTraceContext?.trace_id).toEqual(pageloadTraceContext?.trace_id); + expect(newTraceTransactionTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + }, +); diff --git a/dev-packages/browser-integration-tests/utils/helpers.ts b/dev-packages/browser-integration-tests/utils/helpers.ts index 0e888a708f00..ab55da449ad2 100644 --- a/dev-packages/browser-integration-tests/utils/helpers.ts +++ b/dev-packages/browser-integration-tests/utils/helpers.ts @@ -241,7 +241,7 @@ export function shouldSkipTracingTest(): boolean { } /** - * We can only test replay tests in certain bundles/packages: + * We can only test feedback tests in certain bundles/packages: * - NPM (ESM, CJS) * - CDN bundles that contain the Replay integration * @@ -252,6 +252,18 @@ export function shouldSkipFeedbackTest(): boolean { return bundle != null && !bundle.includes('feedback') && !bundle.includes('esm') && !bundle.includes('cjs'); } +/** + * We can only test metrics tests in certain bundles/packages: + * - NPM (ESM, CJS) + * - CDN bundles that include tracing + * + * @returns `true` if we should skip the metrics test + */ +export function shouldSkipMetricsTest(): boolean { + const bundle = process.env.PW_BUNDLE as string | undefined; + return bundle != null && !bundle.includes('tracing') && !bundle.includes('esm') && !bundle.includes('cjs'); +} + /** * Waits until a number of requests matching urlRgx at the given URL arrive. * If the timout option is configured, this function will abort waiting, even if it hasn't reveived the configured diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/ppr-error/[param]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15/app/ppr-error/[param]/page.tsx new file mode 100644 index 000000000000..ec2b2b1232c7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/ppr-error/[param]/page.tsx @@ -0,0 +1,18 @@ +import * as Sentry from '@sentry/nextjs'; + +export default async function Page({ + searchParams, +}: { + searchParams: { id?: string }; +}) { + try { + console.log(searchParams.id); // Accessing a field on searchParams will throw the PPR error + } catch (e) { + Sentry.captureException(e); // This error should not be reported + await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for any async event processors to run + await Sentry.flush(); + throw e; + } + + return
This server component will throw a PPR error that we do not want to catch.
; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/next.config.js b/dev-packages/e2e-tests/test-applications/nextjs-15/next.config.js index 1098c2ce5a4f..2be749fde774 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/next.config.js +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/next.config.js @@ -1,7 +1,11 @@ const { withSentryConfig } = require('@sentry/nextjs'); /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + experimental: { + ppr: true, + }, +}; module.exports = withSentryConfig(nextConfig, { silent: true, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/ppr-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/ppr-error.test.ts new file mode 100644 index 000000000000..1e266fa02541 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/ppr-error.test.ts @@ -0,0 +1,22 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; + +test('should not capture React-internal errors for PPR rendering', async ({ page }) => { + const pageServerComponentTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { + return transactionEvent?.transaction === 'Page Server Component (/ppr-error/[param])'; + }); + + let errorEventReceived = false; + waitForError('nextjs-15', async transactionEvent => { + return transactionEvent?.transaction === 'Page Server Component (/ppr-error/[param])'; + }).then(() => { + errorEventReceived = true; + }); + + await page.goto(`/ppr-error/foobar?id=1`); + + const pageServerComponentTransaction = await pageServerComponentTransactionPromise; + expect(pageServerComponentTransaction).toBeDefined(); + + expect(errorEventReceived).toBe(false); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts index a3e9ac0852aa..a35bf4657c64 100644 --- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts @@ -18,6 +18,9 @@ const NODE_EXPORTS_IGNORE = [ 'setNodeAsyncContextStrategy', 'getDefaultIntegrationsWithoutPerformance', 'initWithoutDefaultIntegrations', + 'SentryContextManager', + 'validateOpenTelemetrySetup', + 'preloadOpenTelemetry', ]; const nodeExports = Object.keys(SentryNode).filter(e => !NODE_EXPORTS_IGNORE.includes(e)); diff --git a/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/.npmrc b/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/package.json b/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/package.json new file mode 100644 index 000000000000..8d98a54b8d7e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/package.json @@ -0,0 +1,24 @@ +{ + "name": "node-express-cjs-preload", + "version": "1.0.0", + "private": true, + "scripts": { + "start": "node --require @sentry/node/preload src/app.js", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install", + "test:assert": "playwright test" + }, + "dependencies": { + "@sentry/node": "latest || *", + "@sentry/opentelemetry": "latest || *", + "express": "4.19.2" + }, + "devDependencies": { + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", + "@playwright/test": "^1.27.1" + }, + "volta": { + "extends": "../../package.json", + "node": "18.19.1" + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/playwright.config.mjs new file mode 100644 index 000000000000..59b8f10d691b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/playwright.config.mjs @@ -0,0 +1,69 @@ +import { devices } from '@playwright/test'; + +// Fix urls not resolving to localhost on Node v17+ +// See: https://github.com/axios/axios/issues/3821#issuecomment-1413727575 +import { setDefaultResultOrder } from 'dns'; +setDefaultResultOrder('ipv4first'); + +const eventProxyPort = 3031; +const expressPort = 3030; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config = { + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 150_000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000, + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: 0, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'list', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: `http://localhost:${expressPort}`, + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: [ + { + command: 'node start-event-proxy.mjs', + port: eventProxyPort, + stdout: 'pipe', + stderr: 'pipe', + }, + { + command: 'pnpm start', + port: expressPort, + stdout: 'pipe', + stderr: 'pipe', + }, + ], +}; + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/src/app.js b/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/src/app.js new file mode 100644 index 000000000000..b41d99ab6440 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/src/app.js @@ -0,0 +1,52 @@ +const Sentry = require('@sentry/node'); +const express = require('express'); + +const app = express(); +const port = 3030; + +app.get('/test-success', function (req, res) { + setTimeout(() => { + res.status(200).end(); + }, 100); +}); + +app.get('/test-transaction/:param', function (req, res) { + setTimeout(() => { + res.status(200).end(); + }, 100); +}); + +app.get('/test-error', function (req, res) { + Sentry.captureException(new Error('This is an error')); + setTimeout(() => { + Sentry.flush(2000).then(() => { + res.status(200).end(); + }); + }, 100); +}); + +Sentry.setupExpressErrorHandler(app); + +app.use(function onError(err, req, res, next) { + // The error id is attached to `res.sentry` to be returned + // and optionally displayed to the user for support. + res.statusCode = 500; + res.end(res.sentry + '\n'); +}); + +async function run() { + await new Promise(resolve => setTimeout(resolve, 1000)); + + Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1, + }); + + app.listen(port, () => { + console.log(`Example app listening on port ${port}`); + }); +} + +run(); diff --git a/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/start-event-proxy.mjs new file mode 100644 index 000000000000..e2b0f5436f3d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'node-express-cjs-preload', +}); diff --git a/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/tests/server.test.ts b/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/tests/server.test.ts new file mode 100644 index 000000000000..3ca97ad0b207 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/tests/server.test.ts @@ -0,0 +1,123 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; + +test('Should record exceptions captured inside handlers', async ({ request }) => { + const errorEventPromise = waitForError('node-express-cjs-preload', errorEvent => { + return !!errorEvent?.exception?.values?.[0]?.value?.includes('This is an error'); + }); + + await request.get('/test-error'); + + await expect(errorEventPromise).resolves.toBeDefined(); +}); + +test('Should record a transaction for a parameterless route', async ({ request }) => { + const transactionEventPromise = waitForTransaction('node-express-cjs-preload', transactionEvent => { + return transactionEvent?.transaction === 'GET /test-success'; + }); + + await request.get('/test-success'); + + await expect(transactionEventPromise).resolves.toBeDefined(); +}); + +test('Should record a transaction for route with parameters', async ({ request }) => { + const transactionEventPromise = waitForTransaction('node-express-cjs-preload', transactionEvent => { + return transactionEvent.contexts?.trace?.data?.['http.target'] === '/test-transaction/1'; + }); + + await request.get('/test-transaction/1'); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent).toBeDefined(); + expect(transactionEvent.transaction).toEqual('GET /test-transaction/:param'); + expect(transactionEvent.contexts?.trace?.data).toEqual( + expect.objectContaining({ + 'http.flavor': '1.1', + 'http.host': 'localhost:3030', + 'http.method': 'GET', + 'http.response.status_code': 200, + 'http.route': '/test-transaction/:param', + 'http.scheme': 'http', + 'http.status_code': 200, + 'http.status_text': 'OK', + 'http.target': '/test-transaction/1', + 'http.url': 'http://localhost:3030/test-transaction/1', + 'http.user_agent': expect.any(String), + 'net.host.ip': expect.any(String), + 'net.host.name': 'localhost', + 'net.host.port': 3030, + 'net.peer.ip': expect.any(String), + 'net.peer.port': expect.any(Number), + 'net.transport': 'ip_tcp', + 'otel.kind': 'SERVER', + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.otel.http', + 'sentry.sample_rate': 1, + 'sentry.source': 'route', + url: 'http://localhost:3030/test-transaction/1', + }), + ); + + const spans = transactionEvent.spans || []; + expect(spans).toContainEqual({ + data: { + 'express.name': 'query', + 'express.type': 'middleware', + 'http.route': '/', + 'otel.kind': 'INTERNAL', + 'sentry.origin': 'auto.http.otel.express', + 'sentry.op': 'middleware.express', + }, + op: 'middleware.express', + description: 'query', + origin: 'auto.http.otel.express', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + + expect(spans).toContainEqual({ + data: { + 'express.name': 'expressInit', + 'express.type': 'middleware', + 'http.route': '/', + 'otel.kind': 'INTERNAL', + 'sentry.origin': 'auto.http.otel.express', + 'sentry.op': 'middleware.express', + }, + op: 'middleware.express', + description: 'expressInit', + origin: 'auto.http.otel.express', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + + expect(spans).toContainEqual({ + data: { + 'express.name': '/test-transaction/:param', + 'express.type': 'request_handler', + 'http.route': '/test-transaction/:param', + 'otel.kind': 'INTERNAL', + 'sentry.origin': 'auto.http.otel.express', + 'sentry.op': 'request_handler.express', + }, + op: 'request_handler.express', + description: '/test-transaction/:param', + origin: 'auto.http.otel.express', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-preload/.npmrc b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-preload/package.json b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/package.json new file mode 100644 index 000000000000..20bda187d3a2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/package.json @@ -0,0 +1,24 @@ +{ + "name": "node-express-esm-preload", + "version": "1.0.0", + "private": true, + "scripts": { + "start": "node --import @sentry/node/preload src/app.mjs", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install", + "test:assert": "playwright test" + }, + "dependencies": { + "@sentry/node": "latest || *", + "@sentry/opentelemetry": "latest || *", + "express": "4.19.2" + }, + "devDependencies": { + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", + "@playwright/test": "^1.27.1" + }, + "volta": { + "extends": "../../package.json", + "node": "18.19.1" + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-preload/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/playwright.config.mjs new file mode 100644 index 000000000000..59b8f10d691b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/playwright.config.mjs @@ -0,0 +1,69 @@ +import { devices } from '@playwright/test'; + +// Fix urls not resolving to localhost on Node v17+ +// See: https://github.com/axios/axios/issues/3821#issuecomment-1413727575 +import { setDefaultResultOrder } from 'dns'; +setDefaultResultOrder('ipv4first'); + +const eventProxyPort = 3031; +const expressPort = 3030; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config = { + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 150_000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000, + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: 0, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'list', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: `http://localhost:${expressPort}`, + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: [ + { + command: 'node start-event-proxy.mjs', + port: eventProxyPort, + stdout: 'pipe', + stderr: 'pipe', + }, + { + command: 'pnpm start', + port: expressPort, + stdout: 'pipe', + stderr: 'pipe', + }, + ], +}; + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-preload/src/app.mjs b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/src/app.mjs new file mode 100644 index 000000000000..abb70111543d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/src/app.mjs @@ -0,0 +1,52 @@ +import * as Sentry from '@sentry/node'; +import express from 'express'; + +const app = express(); +const port = 3030; + +app.get('/test-success', function (req, res) { + setTimeout(() => { + res.status(200).end(); + }, 100); +}); + +app.get('/test-transaction/:param', function (req, res) { + setTimeout(() => { + res.status(200).end(); + }, 100); +}); + +app.get('/test-error', function (req, res) { + Sentry.captureException(new Error('This is an error')); + setTimeout(() => { + Sentry.flush(2000).then(() => { + res.status(200).end(); + }); + }, 100); +}); + +Sentry.setupExpressErrorHandler(app); + +app.use(function onError(err, req, res, next) { + // The error id is attached to `res.sentry` to be returned + // and optionally displayed to the user for support. + res.statusCode = 500; + res.end(res.sentry + '\n'); +}); + +async function run() { + await new Promise(resolve => setTimeout(resolve, 1000)); + + Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1, + }); + + app.listen(port, () => { + console.log(`Example app listening on port ${port}`); + }); +} + +run(); diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-preload/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/start-event-proxy.mjs new file mode 100644 index 000000000000..6b5d011dcb03 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'node-express-esm-preload', +}); diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-preload/tests/server.test.ts b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/tests/server.test.ts new file mode 100644 index 000000000000..19803d7b3a7f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/tests/server.test.ts @@ -0,0 +1,123 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; + +test('Should record exceptions captured inside handlers', async ({ request }) => { + const errorEventPromise = waitForError('node-express-esm-preload', errorEvent => { + return !!errorEvent?.exception?.values?.[0]?.value?.includes('This is an error'); + }); + + await request.get('/test-error'); + + await expect(errorEventPromise).resolves.toBeDefined(); +}); + +test('Should record a transaction for a parameterless route', async ({ request }) => { + const transactionEventPromise = waitForTransaction('node-express-esm-preload', transactionEvent => { + return transactionEvent?.transaction === 'GET /test-success'; + }); + + await request.get('/test-success'); + + await expect(transactionEventPromise).resolves.toBeDefined(); +}); + +test('Should record a transaction for route with parameters', async ({ request }) => { + const transactionEventPromise = waitForTransaction('node-express-esm-preload', transactionEvent => { + return transactionEvent.contexts?.trace?.data?.['http.target'] === '/test-transaction/1'; + }); + + await request.get('/test-transaction/1'); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent).toBeDefined(); + expect(transactionEvent.transaction).toEqual('GET /test-transaction/:param'); + expect(transactionEvent.contexts?.trace?.data).toEqual( + expect.objectContaining({ + 'http.flavor': '1.1', + 'http.host': 'localhost:3030', + 'http.method': 'GET', + 'http.response.status_code': 200, + 'http.route': '/test-transaction/:param', + 'http.scheme': 'http', + 'http.status_code': 200, + 'http.status_text': 'OK', + 'http.target': '/test-transaction/1', + 'http.url': 'http://localhost:3030/test-transaction/1', + 'http.user_agent': expect.any(String), + 'net.host.ip': expect.any(String), + 'net.host.name': 'localhost', + 'net.host.port': 3030, + 'net.peer.ip': expect.any(String), + 'net.peer.port': expect.any(Number), + 'net.transport': 'ip_tcp', + 'otel.kind': 'SERVER', + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.otel.http', + 'sentry.sample_rate': 1, + 'sentry.source': 'route', + url: 'http://localhost:3030/test-transaction/1', + }), + ); + + const spans = transactionEvent.spans || []; + expect(spans).toContainEqual({ + data: { + 'express.name': 'query', + 'express.type': 'middleware', + 'http.route': '/', + 'otel.kind': 'INTERNAL', + 'sentry.origin': 'auto.http.otel.express', + 'sentry.op': 'middleware.express', + }, + op: 'middleware.express', + description: 'query', + origin: 'auto.http.otel.express', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + + expect(spans).toContainEqual({ + data: { + 'express.name': 'expressInit', + 'express.type': 'middleware', + 'http.route': '/', + 'otel.kind': 'INTERNAL', + 'sentry.origin': 'auto.http.otel.express', + 'sentry.op': 'middleware.express', + }, + op: 'middleware.express', + description: 'expressInit', + origin: 'auto.http.otel.express', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + + expect(spans).toContainEqual({ + data: { + 'express.name': '/test-transaction/:param', + 'express.type': 'request_handler', + 'http.route': '/test-transaction/:param', + 'otel.kind': 'INTERNAL', + 'sentry.origin': 'auto.http.otel.express', + 'sentry.op': 'request_handler.express', + }, + op: 'request_handler.express', + description: '/test-transaction/:param', + origin: 'auto.http.otel.express', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/test/performance.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/test/performance.test.ts index 8ef420eb8eb1..83932a4ac362 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/test/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/test/performance.test.ts @@ -185,8 +185,6 @@ test.describe('performance events', () => { }); test('captures a navigation transaction directly after pageload', async ({ page }) => { - await page.goto('/'); - const clientPageloadTxnPromise = waitForTransaction('sveltekit-2-svelte-5', txnEvent => { return txnEvent?.contexts?.trace?.op === 'pageload' && txnEvent?.tags?.runtime === 'browser'; }); @@ -195,6 +193,8 @@ test.describe('performance events', () => { return txnEvent?.contexts?.trace?.op === 'navigation' && txnEvent?.tags?.runtime === 'browser'; }); + await page.goto('/'); + const navigationClickPromise = page.locator('#routeWithParamsLink').click(); const [pageloadTxnEvent, navigationTxnEvent, _] = await Promise.all([ diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.test.ts index adfeb4e31d85..e2966f23fb8b 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.test.ts @@ -185,8 +185,6 @@ test.describe('performance events', () => { }); test('captures a navigation transaction directly after pageload', async ({ page }) => { - await page.goto('/'); - const clientPageloadTxnPromise = waitForTransaction('sveltekit-2', txnEvent => { return txnEvent?.contexts?.trace?.op === 'pageload' && txnEvent?.tags?.runtime === 'browser'; }); @@ -195,6 +193,8 @@ test.describe('performance events', () => { return txnEvent?.contexts?.trace?.op === 'navigation' && txnEvent?.tags?.runtime === 'browser'; }); + await page.goto('/'); + const navigationClickPromise = page.locator('#routeWithParamsLink').click(); const [pageloadTxnEvent, navigationTxnEvent, _] = await Promise.all([ diff --git a/dev-packages/e2e-tests/verdaccio-config/config.yaml b/dev-packages/e2e-tests/verdaccio-config/config.yaml index 8638c7fb170a..d9b11385dfa2 100644 --- a/dev-packages/e2e-tests/verdaccio-config/config.yaml +++ b/dev-packages/e2e-tests/verdaccio-config/config.yaml @@ -128,6 +128,12 @@ packages: unpublish: $all # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/solidjs': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/svelte': access: $all publish: $all diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/deny-inspector.mjs b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/deny-inspector.mjs new file mode 100644 index 000000000000..99323e91f0bc --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/deny-inspector.mjs @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/node'; +import Hook from 'import-in-the-middle'; + +Hook((_, name) => { + if (name === 'inspector') { + throw new Error('No inspector!'); + } + if (name === 'node:inspector') { + throw new Error('No inspector!'); + } +}); + +Sentry.init({}); diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts index 61b9fc3064a4..0ad4ddad7c5a 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts @@ -76,6 +76,14 @@ conditionalTest({ min: 18 })('LocalVariables integration', () => { .start(done); }); + test('Should not import inspector when not in use', done => { + createRunner(__dirname, 'deny-inspector.mjs') + .withFlags('--import=@sentry/node/import') + .ensureNoErrorOutput() + .ignore('session') + .start(done); + }); + test('Includes local variables for caught exceptions when enabled', done => { createRunner(__dirname, 'local-variables-caught.js') .ignore('session') diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts b/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts index 0c2beaf7d4c8..3ad860bb72f4 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts @@ -1,6 +1,6 @@ import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; -describe('redis auto instrumentation', () => { +describe('redis cache auto instrumentation', () => { afterAll(() => { cleanupChildProcesses(); }); @@ -12,6 +12,7 @@ describe('redis auto instrumentation', () => { expect.objectContaining({ description: 'set test-key [1 other arguments]', op: 'db', + origin: 'auto.db.otel.redis', data: expect.objectContaining({ 'sentry.op': 'db', 'db.system': 'redis', @@ -23,6 +24,7 @@ describe('redis auto instrumentation', () => { expect.objectContaining({ description: 'get test-key', op: 'db', + origin: 'auto.db.otel.redis', data: expect.objectContaining({ 'sentry.op': 'db', 'db.system': 'redis', @@ -48,6 +50,7 @@ describe('redis auto instrumentation', () => { expect.objectContaining({ description: 'set ioredis-cache:test-key [1 other arguments]', op: 'cache.put', + origin: 'auto.db.otel.redis', data: expect.objectContaining({ 'db.statement': 'set ioredis-cache:test-key [1 other arguments]', 'cache.key': 'ioredis-cache:test-key', @@ -60,6 +63,7 @@ describe('redis auto instrumentation', () => { expect.objectContaining({ description: 'get ioredis-cache:test-key', op: 'cache.get_item', // todo: will be changed to cache.get + origin: 'auto.db.otel.redis', data: expect.objectContaining({ 'db.statement': 'get ioredis-cache:test-key', 'cache.hit': true, @@ -73,6 +77,7 @@ describe('redis auto instrumentation', () => { expect.objectContaining({ description: 'get ioredis-cache:unavailable-data', op: 'cache.get_item', // todo: will be changed to cache.get + origin: 'auto.db.otel.redis', data: expect.objectContaining({ 'db.statement': 'get ioredis-cache:unavailable-data', 'cache.hit': false, diff --git a/dev-packages/node-integration-tests/suites/tracing/redis/test.ts b/dev-packages/node-integration-tests/suites/tracing/redis/test.ts index 604b2751f05b..f68c14499a13 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/redis/test.ts @@ -12,8 +12,10 @@ describe('redis auto instrumentation', () => { expect.objectContaining({ description: 'set test-key [1 other arguments]', op: 'db', + origin: 'auto.db.otel.redis', data: expect.objectContaining({ 'sentry.op': 'db', + 'sentry.origin': 'auto.db.otel.redis', 'db.system': 'redis', 'net.peer.name': 'localhost', 'net.peer.port': 6379, @@ -23,8 +25,10 @@ describe('redis auto instrumentation', () => { expect.objectContaining({ description: 'get test-key', op: 'db', + origin: 'auto.db.otel.redis', data: expect.objectContaining({ 'sentry.op': 'db', + 'sentry.origin': 'auto.db.otel.redis', 'db.system': 'redis', 'net.peer.name': 'localhost', 'net.peer.port': 6379, diff --git a/dev-packages/rollup-utils/bundleHelpers.mjs b/dev-packages/rollup-utils/bundleHelpers.mjs index ae6574c0b0bc..9e507f7f65fc 100644 --- a/dev-packages/rollup-utils/bundleHelpers.mjs +++ b/dev-packages/rollup-utils/bundleHelpers.mjs @@ -27,7 +27,7 @@ export function makeBaseBundleConfig(options) { const { bundleType, entrypoints, licenseTitle, outputFileBase, packageSpecificConfig, sucrase } = options; const nodeResolvePlugin = makeNodeResolvePlugin(); - const sucrasePlugin = makeSucrasePlugin(sucrase); + const sucrasePlugin = makeSucrasePlugin({}, sucrase); const cleanupPlugin = makeCleanupPlugin(); const markAsBrowserBuildPlugin = makeBrowserBuildPlugin(true); const licensePlugin = makeLicensePlugin(licenseTitle); @@ -88,11 +88,27 @@ export function makeBaseBundleConfig(options) { }; // used by `@sentry/aws-serverless`, when creating the lambda layer - const nodeBundleConfig = { + const awsLambdaBundleConfig = { output: { format: 'cjs', }, - plugins: [jsonPlugin, commonJSPlugin], + plugins: [ + jsonPlugin, + commonJSPlugin, + // Temporary fix for the lambda layer SDK bundle. + // This is necessary to apply to our lambda layer bundle because calling `new ImportInTheMiddle()` will throw an + // that `ImportInTheMiddle` is not a constructor. Instead we modify the code to call `new ImportInTheMiddle.default()` + // TODO: Remove this plugin once the weird import-in-the-middle exports are fixed, released and we use the respective + // version in our SDKs. See: https://github.com/getsentry/sentry-javascript/issues/12009#issuecomment-2126211967 + { + name: 'aws-serverless-lambda-layer-fix', + transform: code => { + if (code.includes('ImportInTheMiddle')) { + return code.replaceAll(/new\s+(ImportInTheMiddle.*)\(/gm, 'new $1.default('); + } + }, + }, + ], // Don't bundle any of Node's core modules external: builtinModules, }; @@ -124,7 +140,7 @@ export function makeBaseBundleConfig(options) { const bundleTypeConfigMap = { standalone: standAloneBundleConfig, addon: addOnBundleConfig, - node: nodeBundleConfig, + 'aws-lambda': awsLambdaBundleConfig, 'node-worker': workerBundleConfig, }; diff --git a/dev-packages/rollup-utils/npmHelpers.mjs b/dev-packages/rollup-utils/npmHelpers.mjs index 0347c1d9e12b..5be51096cf75 100644 --- a/dev-packages/rollup-utils/npmHelpers.mjs +++ b/dev-packages/rollup-utils/npmHelpers.mjs @@ -40,7 +40,7 @@ export function makeBaseNPMConfig(options = {}) { } = options; const nodeResolvePlugin = makeNodeResolvePlugin(); - const sucrasePlugin = makeSucrasePlugin({ disableESTransforms: !addPolyfills, ...sucrase }); + const sucrasePlugin = makeSucrasePlugin({}, { disableESTransforms: !addPolyfills, ...sucrase }); const debugBuildStatementReplacePlugin = makeDebugBuildStatementReplacePlugin(); const importMetaUrlReplacePlugin = makeImportMetaUrlReplacePlugin(); const cleanupPlugin = makeCleanupPlugin(); diff --git a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs index 2404e3fc2d38..169062694d24 100644 --- a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs +++ b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs @@ -128,6 +128,8 @@ export function makeTerserPlugin() { '_sentrySpan', '_sentryScope', '_sentryIsolationScope', + // require-in-the-middle calls `Module._resolveFilename`. We cannot mangle this (AWS lambda layer bundle). + '_resolveFilename', ], }, }, diff --git a/dev-packages/rollup-utils/plugins/npmPlugins.mjs b/dev-packages/rollup-utils/plugins/npmPlugins.mjs index 89a0cfda7bb9..fd736d702e07 100644 --- a/dev-packages/rollup-utils/plugins/npmPlugins.mjs +++ b/dev-packages/rollup-utils/plugins/npmPlugins.mjs @@ -13,21 +13,26 @@ import * as path from 'path'; import { codecovRollupPlugin } from '@codecov/rollup-plugin'; import json from '@rollup/plugin-json'; import replace from '@rollup/plugin-replace'; -import sucrase from '@rollup/plugin-sucrase'; import cleanup from 'rollup-plugin-cleanup'; +import sucrase from './vendor/sucrase-plugin.mjs'; /** * Create a plugin to transpile TS syntax using `sucrase`. * * @returns An instance of the `@rollup/plugin-sucrase` plugin */ -export function makeSucrasePlugin(options = {}) { - return sucrase({ - // Required for bundling OTEL code properly - exclude: ['**/*.json'], - transforms: ['typescript', 'jsx'], - ...options, - }); +export function makeSucrasePlugin(options = {}, sucraseOptions = {}) { + return sucrase( + { + // Required for bundling OTEL code properly + exclude: ['**/*.json'], + ...options, + }, + { + transforms: ['typescript', 'jsx'], + ...sucraseOptions, + }, + ); } export function makeJsonPlugin() { diff --git a/dev-packages/rollup-utils/plugins/vendor/sucrase-plugin.mjs b/dev-packages/rollup-utils/plugins/vendor/sucrase-plugin.mjs new file mode 100644 index 000000000000..63465e768bc9 --- /dev/null +++ b/dev-packages/rollup-utils/plugins/vendor/sucrase-plugin.mjs @@ -0,0 +1,79 @@ +// Vendored from https://github.com/rollup/plugins/blob/0090e728f52828d39b071ab5c7925b9b575cd568/packages/sucrase/src/index.js and modified + +/* + +The MIT License (MIT) + +Copyright (c) 2019 RollupJS Plugin Contributors (https://github.com/rollup/plugins/graphs/contributors) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +import fs from 'fs'; +import path from 'path'; + +import { createFilter } from '@rollup/pluginutils'; +import { transform } from 'sucrase'; + +export default function sucrase(opts = {}, sucraseOpts = {}) { + const filter = createFilter(opts.include, opts.exclude); + + return { + name: 'sucrase', + + // eslint-disable-next-line consistent-return + resolveId(importee, importer) { + if (importer && /^[./]/.test(importee)) { + const resolved = path.resolve(importer ? path.dirname(importer) : process.cwd(), importee); + // resolve in the same order that TypeScript resolves modules + const resolvedFilenames = [ + `${resolved}.ts`, + `${resolved}.tsx`, + `${resolved}/index.ts`, + `${resolved}/index.tsx`, + ]; + if (resolved.endsWith('.js')) { + resolvedFilenames.splice(2, 0, `${resolved.slice(0, -3)}.ts`, `${resolved.slice(0, -3)}.tsx`); + } + const resolvedFilename = resolvedFilenames.find(filename => fs.existsSync(filename)); + + if (resolvedFilename) { + return resolvedFilename; + } + } + }, + + transform(code, id) { + if (!filter(id)) return null; + const result = transform(code, { + transforms: sucraseOpts.transforms, + filePath: id, + sourceMapOptions: { + compiledFilename: id, + }, + ...sucraseOpts, + }); + return { + code: result.code, + map: result.sourceMap, + }; + }, + }; +} diff --git a/package.json b/package.json index 78bb0f5d0788..70b7663667f4 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "packages/replay-internal", "packages/replay-canvas", "packages/replay-worker", + "packages/solidjs", "packages/svelte", "packages/sveltekit", "packages/types", @@ -97,6 +98,7 @@ "@rollup/plugin-sucrase": "^5.0.2", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^11.1.6", + "@rollup/pluginutils": "^5.1.0", "@size-limit/file": "~11.1.0", "@size-limit/webpack": "~11.1.0", "@strictsoftware/typedoc-plugin-monorepo": "^0.3.1", @@ -124,6 +126,7 @@ "rollup-plugin-cleanup": "^3.2.1", "rollup-plugin-license": "^3.3.1", "size-limit": "~11.1.0", + "sucrase": "^3.35.0", "ts-jest": "^27.1.4", "ts-node": "10.9.1", "typedoc": "^0.18.0", diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index a4f3ca59fb1f..6657b3030cb1 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -64,6 +64,7 @@ export { startSpan, startInactiveSpan, startSpanManual, + startNewTrace, withActiveSpan, getSpanDescendants, continueTrace, diff --git a/packages/aws-serverless/rollup.aws.config.mjs b/packages/aws-serverless/rollup.aws.config.mjs index 6f5cc7d581e5..6348d3b1ae74 100644 --- a/packages/aws-serverless/rollup.aws.config.mjs +++ b/packages/aws-serverless/rollup.aws.config.mjs @@ -5,7 +5,7 @@ export default [ ...makeBundleConfigVariants( makeBaseBundleConfig({ // this automatically sets it to be CJS - bundleType: 'node', + bundleType: 'aws-lambda', entrypoints: ['src/index.ts'], licenseTitle: '@sentry/aws-serverless', outputFileBase: () => 'index', @@ -15,7 +15,6 @@ export default [ sourcemap: false, }, }, - preserveModules: false, }), // We only need one copy of the SDK, and we pick the minified one because there's a cap on how big a lambda function // plus its dependencies can be, and we might as well take up as little of that space as is necessary. We'll rename diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 1d2323df06e5..62165a710127 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -62,6 +62,7 @@ export { startSpan, startInactiveSpan, startSpanManual, + startNewTrace, withActiveSpan, getRootSpan, getSpanDescendants, diff --git a/packages/browser-utils/src/metrics/inp.ts b/packages/browser-utils/src/metrics/inp.ts index eeead7b12018..c6c0113d6be3 100644 --- a/packages/browser-utils/src/metrics/inp.ts +++ b/packages/browser-utils/src/metrics/inp.ts @@ -93,7 +93,13 @@ function _trackINP(): () => void { const replayId = replay && replay.getReplayId(); const userDisplay = user !== undefined ? user.email || user.id || user.ip_address : undefined; - const profileId = scope.getScopeData().contexts?.profile?.profile_id as string | undefined; + let profileId: string | undefined = undefined; + try { + // @ts-expect-error skip optional chaining to save bundle size with try catch + profileId = scope.getScopeData().contexts.profile.profile_id; + } catch { + // do nothing + } const name = htmlTreeAsString(entry.target); const attributes: SpanAttributes = dropUndefinedKeys({ diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index 6b8edb66541a..94b4bd0b3c2a 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -68,8 +68,6 @@ export { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, } from '@sentry/core'; -export * from './metrics'; - export { WINDOW } from './helpers'; export { BrowserClient } from './client'; export { makeFetchTransport } from './transports/fetch'; diff --git a/packages/browser/src/index.bundle.feedback.ts b/packages/browser/src/index.bundle.feedback.ts index 957583d79eeb..c6f75c03d9d1 100644 --- a/packages/browser/src/index.bundle.feedback.ts +++ b/packages/browser/src/index.bundle.feedback.ts @@ -1,4 +1,4 @@ -import { browserTracingIntegrationShim, replayIntegrationShim } from '@sentry-internal/integration-shims'; +import { browserTracingIntegrationShim, metricsShim, replayIntegrationShim } from '@sentry-internal/integration-shims'; import { feedbackAsyncIntegration } from './feedbackAsync'; export * from './index.bundle.base'; @@ -10,6 +10,7 @@ export { feedbackAsyncIntegration as feedbackAsyncIntegration, feedbackAsyncIntegration as feedbackIntegration, replayIntegrationShim as replayIntegration, + metricsShim as metrics, }; export { captureFeedback } from '@sentry/core'; diff --git a/packages/browser/src/index.bundle.replay.ts b/packages/browser/src/index.bundle.replay.ts index 1a538a97162f..1f1d4441346b 100644 --- a/packages/browser/src/index.bundle.replay.ts +++ b/packages/browser/src/index.bundle.replay.ts @@ -1,4 +1,8 @@ -import { browserTracingIntegrationShim, feedbackIntegrationShim } from '@sentry-internal/integration-shims'; +import { + browserTracingIntegrationShim, + feedbackIntegrationShim, + metricsShim, +} from '@sentry-internal/integration-shims'; export * from './index.bundle.base'; @@ -8,4 +12,5 @@ export { browserTracingIntegrationShim as browserTracingIntegration, feedbackIntegrationShim as feedbackAsyncIntegration, feedbackIntegrationShim as feedbackIntegration, + metricsShim as metrics, }; diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.ts index de8453db8784..29438387ee5b 100644 --- a/packages/browser/src/index.bundle.tracing.replay.feedback.ts +++ b/packages/browser/src/index.bundle.tracing.replay.feedback.ts @@ -4,12 +4,15 @@ registerSpanErrorInstrumentation(); export * from './index.bundle.base'; +export * from './metrics'; + export { getActiveSpan, getRootSpan, startSpan, startInactiveSpan, startSpanManual, + startNewTrace, withActiveSpan, getSpanDescendants, setMeasurement, diff --git a/packages/browser/src/index.bundle.tracing.replay.ts b/packages/browser/src/index.bundle.tracing.replay.ts index 3b8a51e661dc..6520aa185b2f 100644 --- a/packages/browser/src/index.bundle.tracing.replay.ts +++ b/packages/browser/src/index.bundle.tracing.replay.ts @@ -4,12 +4,15 @@ registerSpanErrorInstrumentation(); export * from './index.bundle.base'; +export * from './metrics'; + export { getActiveSpan, getRootSpan, startSpan, startInactiveSpan, startSpanManual, + startNewTrace, withActiveSpan, getSpanDescendants, setMeasurement, diff --git a/packages/browser/src/index.bundle.tracing.ts b/packages/browser/src/index.bundle.tracing.ts index e93bf68994e3..8115e628aa89 100644 --- a/packages/browser/src/index.bundle.tracing.ts +++ b/packages/browser/src/index.bundle.tracing.ts @@ -5,12 +5,15 @@ registerSpanErrorInstrumentation(); export * from './index.bundle.base'; +export * from './metrics'; + export { getActiveSpan, getRootSpan, startSpan, startInactiveSpan, startSpanManual, + startNewTrace, withActiveSpan, getSpanDescendants, setMeasurement, diff --git a/packages/browser/src/index.bundle.ts b/packages/browser/src/index.bundle.ts index 5004b376cd46..38787264f9b0 100644 --- a/packages/browser/src/index.bundle.ts +++ b/packages/browser/src/index.bundle.ts @@ -1,6 +1,7 @@ import { browserTracingIntegrationShim, feedbackIntegrationShim, + metricsShim, replayIntegrationShim, } from '@sentry-internal/integration-shims'; @@ -11,4 +12,5 @@ export { feedbackIntegrationShim as feedbackAsyncIntegration, feedbackIntegrationShim as feedbackIntegration, replayIntegrationShim as replayIntegration, + metricsShim as metrics, }; diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 86e6ea20fe81..9f8ea02e5822 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -39,6 +39,8 @@ export { sendFeedback, } from '@sentry-internal/feedback'; +export * from './metrics'; + export { defaultRequestInstrumentationOptions, instrumentOutgoingRequests, @@ -59,6 +61,7 @@ export { startInactiveSpan, startSpanManual, withActiveSpan, + startNewTrace, getSpanDescendants, setMeasurement, getSpanStatusFromHttpCode, diff --git a/packages/browser/src/metrics.ts b/packages/browser/src/metrics.ts index 6a7792a16c67..267fe90d03c9 100644 --- a/packages/browser/src/metrics.ts +++ b/packages/browser/src/metrics.ts @@ -1,5 +1,5 @@ -import type { MetricData } from '@sentry/core'; import { BrowserMetricsAggregator, metrics as metricsCore } from '@sentry/core'; +import type { MetricData, Metrics } from '@sentry/types'; /** * Adds a value to a counter metric @@ -37,7 +37,7 @@ function gauge(name: string, value: number, data?: MetricData): void { metricsCore.gauge(BrowserMetricsAggregator, name, value, data); } -export const metrics = { +export const metrics: Metrics = { increment, distribution, set, diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index ba058c966754..ea24a475d959 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -60,22 +60,32 @@ function applyDefaultOptions(optionsArg: BrowserOptions = {}): BrowserOptions { return { ...defaultOptions, ...optionsArg }; } +type ExtensionProperties = { + chrome?: Runtime; + browser?: Runtime; +}; +type Runtime = { + runtime?: { + id?: string; + }; +}; + function shouldShowBrowserExtensionError(): boolean { - const windowWithMaybeChrome = WINDOW as typeof WINDOW & { chrome?: { runtime?: { id?: string } } }; - const isInsideChromeExtension = - windowWithMaybeChrome && - windowWithMaybeChrome.chrome && - windowWithMaybeChrome.chrome.runtime && - windowWithMaybeChrome.chrome.runtime.id; - - const windowWithMaybeBrowser = WINDOW as typeof WINDOW & { browser?: { runtime?: { id?: string } } }; - const isInsideBrowserExtension = - windowWithMaybeBrowser && - windowWithMaybeBrowser.browser && - windowWithMaybeBrowser.browser.runtime && - windowWithMaybeBrowser.browser.runtime.id; - - return !!isInsideBrowserExtension || !!isInsideChromeExtension; + const windowWithMaybeExtension = WINDOW as typeof WINDOW & ExtensionProperties; + + const extensionKey = windowWithMaybeExtension.chrome ? 'chrome' : 'browser'; + const extensionObject = windowWithMaybeExtension[extensionKey]; + + const runtimeId = extensionObject && extensionObject.runtime && extensionObject.runtime.id; + const href = (WINDOW.location && WINDOW.location.href) || ''; + + const extensionProtocols = ['chrome-extension:', 'moz-extension:', 'ms-browser-extension:']; + + // Running the SDK in a dedicated extension page and calling Sentry.init is fine; no risk of data leakage + const isDedicatedExtensionPage = + !!runtimeId && WINDOW === WINDOW.top && extensionProtocols.some(protocol => href.startsWith(`${protocol}//`)); + + return !!runtimeId && !isDedicatedExtensionPage; } /** diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index b3d530ee3653..f6528e4d155d 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -27,10 +27,10 @@ import type { Client, IntegrationFn, StartSpanOptions, TransactionSource } from import type { Span } from '@sentry/types'; import { browserPerformanceTimeOrigin, + generatePropagationContext, getDomElement, logger, propagationContextFromHeaders, - uuid4, } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; @@ -412,8 +412,8 @@ export function startBrowserTracingPageLoadSpan( * This will only do something if a browser tracing integration has been setup. */ export function startBrowserTracingNavigationSpan(client: Client, spanOptions: StartSpanOptions): Span | undefined { - getCurrentScope().setPropagationContext(generatePropagationContext()); getIsolationScope().setPropagationContext(generatePropagationContext()); + getCurrentScope().setPropagationContext(generatePropagationContext()); client.emit('startNavigationSpan', spanOptions); @@ -487,10 +487,3 @@ function registerInteractionListener( addEventListener('click', registerInteractionTransaction, { once: false, capture: true }); } } - -function generatePropagationContext(): { traceId: string; spanId: string } { - return { - traceId: uuid4(), - spanId: uuid4().substring(16), - }; -} diff --git a/packages/browser/test/unit/sdk.test.ts b/packages/browser/test/unit/sdk.test.ts index f8f5125ff896..b0af70fcd652 100644 --- a/packages/browser/test/unit/sdk.test.ts +++ b/packages/browser/test/unit/sdk.test.ts @@ -135,6 +135,8 @@ describe('init', () => { new MockIntegration('MockIntegration 0.2'), ]; + const originalLocation = WINDOW.location || {}; + const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, defaultIntegrations: DEFAULT_INTEGRATIONS }); afterEach(() => { @@ -142,7 +144,7 @@ describe('init', () => { Object.defineProperty(WINDOW, 'browser', { value: undefined, writable: true }); }); - it('should log a browser extension error if executed inside a Chrome extension', () => { + it('logs a browser extension error if executed inside a Chrome extension', () => { const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); Object.defineProperty(WINDOW, 'chrome', { @@ -160,7 +162,7 @@ describe('init', () => { consoleErrorSpy.mockRestore(); }); - it('should log a browser extension error if executed inside a Firefox/Safari extension', () => { + it('logs a browser extension error if executed inside a Firefox/Safari extension', () => { const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); Object.defineProperty(WINDOW, 'browser', { value: { runtime: { id: 'mock-extension-id' } }, writable: true }); @@ -175,7 +177,30 @@ describe('init', () => { consoleErrorSpy.mockRestore(); }); - it('should not log a browser extension error if executed inside regular browser environment', () => { + it.each(['chrome-extension', 'moz-extension', 'ms-browser-extension'])( + "doesn't log a browser extension error if executed inside an extension running in a dedicated page (%s)", + extensionProtocol => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // @ts-expect-error - this is a hack to simulate a dedicated page in a browser extension + delete WINDOW.location; + // @ts-expect-error - this is a hack to simulate a dedicated page in a browser extension + WINDOW.location = { + href: `${extensionProtocol}://mock-extension-id/dedicated-page.html`, + }; + + Object.defineProperty(WINDOW, 'browser', { value: { runtime: { id: 'mock-extension-id' } }, writable: true }); + + init(options); + + expect(consoleErrorSpy).toBeCalledTimes(0); + + consoleErrorSpy.mockRestore(); + WINDOW.location = originalLocation; + }, + ); + + it("doesn't log a browser extension error if executed inside regular browser environment", () => { const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); init(options); diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index fd7671b34b09..c3e8eff8beac 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -82,6 +82,7 @@ export { startSpan, startInactiveSpan, startSpanManual, + startNewTrace, withActiveSpan, getRootSpan, getSpanDescendants, diff --git a/packages/core/src/feedback.ts b/packages/core/src/feedback.ts index ae3abc7ca50f..9bd583a288bb 100644 --- a/packages/core/src/feedback.ts +++ b/packages/core/src/feedback.ts @@ -8,11 +8,10 @@ import { getClient, getCurrentScope } from './currentScopes'; export function captureFeedback( feedbackParams: SendFeedbackParams, hint: EventHint & { includeReplay?: boolean } = {}, + scope = getCurrentScope(), ): string { const { message, name, email, url, source, associatedEventId } = feedbackParams; - const client = getClient(); - const feedbackEvent: FeedbackEvent = { contexts: { feedback: dropUndefinedKeys({ @@ -28,11 +27,13 @@ export function captureFeedback( level: 'info', }; + const client = (scope && scope.getClient()) || getClient(); + if (client) { client.emit('beforeSendFeedback', feedbackEvent, hint); } - const eventId = getCurrentScope().captureEvent(feedbackEvent, hint); + const eventId = scope.captureEvent(feedbackEvent, hint); return eventId; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f193f75bfe60..03dfa8e63aa3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -97,7 +97,7 @@ export { rewriteFramesIntegration } from './integrations/rewriteframes'; export { sessionTimingIntegration } from './integrations/sessiontiming'; export { zodErrorsIntegration } from './integrations/zoderrors'; export { metrics } from './metrics/exports'; -export type { MetricData } from './metrics/exports'; +export type { MetricData } from '@sentry/types'; export { metricsDefault } from './metrics/exports-default'; export { BrowserMetricsAggregator } from './metrics/browser-aggregator'; export { getMetricSummaryJsonForSpan } from './metrics/metric-summary'; diff --git a/packages/core/src/metrics/exports-default.ts b/packages/core/src/metrics/exports-default.ts index 280d1d619bea..86d294d059d8 100644 --- a/packages/core/src/metrics/exports-default.ts +++ b/packages/core/src/metrics/exports-default.ts @@ -1,6 +1,5 @@ -import type { Client, MetricsAggregator as MetricsAggregatorInterface } from '@sentry/types'; +import type { Client, MetricData, Metrics, MetricsAggregator as MetricsAggregatorInterface } from '@sentry/types'; import { MetricsAggregator } from './aggregator'; -import type { MetricData } from './exports'; import { metrics as metricsCore } from './exports'; /** @@ -46,11 +45,14 @@ function getMetricsAggregatorForClient(client: Client): MetricsAggregatorInterfa return metricsCore.getMetricsAggregatorForClient(client, MetricsAggregator); } -export const metricsDefault = { +export const metricsDefault: Metrics & { + getMetricsAggregatorForClient: typeof getMetricsAggregatorForClient; +} = { increment, distribution, set, gauge, + /** * @ignore This is for internal use only. */ diff --git a/packages/core/src/metrics/exports.ts b/packages/core/src/metrics/exports.ts index f062b65f72d9..665ac9c12816 100644 --- a/packages/core/src/metrics/exports.ts +++ b/packages/core/src/metrics/exports.ts @@ -1,9 +1,4 @@ -import type { - Client, - MeasurementUnit, - MetricsAggregator as MetricsAggregatorInterface, - Primitive, -} from '@sentry/types'; +import type { Client, MetricData, MetricsAggregator as MetricsAggregatorInterface } from '@sentry/types'; import { getGlobalSingleton, logger } from '@sentry/utils'; import { getClient } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; @@ -11,13 +6,6 @@ import { getActiveSpan, getRootSpan, spanToJSON } from '../utils/spanUtils'; import { COUNTER_METRIC_TYPE, DISTRIBUTION_METRIC_TYPE, GAUGE_METRIC_TYPE, SET_METRIC_TYPE } from './constants'; import type { MetricType } from './types'; -export interface MetricData { - unit?: MeasurementUnit; - tags?: Record; - timestamp?: number; - client?: Client; -} - type MetricsAggregatorConstructor = { new (client: Client): MetricsAggregatorInterface; }; diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index 724c9b621ce9..ff89c0d593a9 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -21,7 +21,7 @@ import type { SeverityLevel, User, } from '@sentry/types'; -import { dateTimestampInSeconds, isPlainObject, logger, uuid4 } from '@sentry/utils'; +import { dateTimestampInSeconds, generatePropagationContext, isPlainObject, logger, uuid4 } from '@sentry/utils'; import { updateSession } from './session'; import { _getSpanForScope, _setSpanForScope } from './utils/spanOnScope'; @@ -600,10 +600,3 @@ export const Scope = ScopeClass; * Holds additional event information. */ export type Scope = ScopeInterface; - -function generatePropagationContext(): PropagationContext { - return { - traceId: uuid4(), - spanId: uuid4().substring(16), - }; -} diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts index 90a5ac737aa1..0c08101acb68 100644 --- a/packages/core/src/tracing/index.ts +++ b/packages/core/src/tracing/index.ts @@ -17,6 +17,7 @@ export { continueTrace, withActiveSpan, suppressTracing, + startNewTrace, } from './trace'; export { getDynamicSamplingContextFromClient, diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 4d910f54e996..e34c2c1a62d3 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -1,11 +1,12 @@ import type { ClientOptions, Scope, SentrySpanArguments, Span, SpanTimeInput, StartSpanOptions } from '@sentry/types'; -import { propagationContextFromHeaders } from '@sentry/utils'; +import { generatePropagationContext, logger, propagationContextFromHeaders } from '@sentry/utils'; import type { AsyncContextStrategy } from '../asyncContext/types'; import { getMainCarrier } from '../carrier'; import { getClient, getCurrentScope, getIsolationScope, withScope } from '../currentScopes'; import { getAsyncContextStrategy } from '../asyncContext'; +import { DEBUG_BUILD } from '../debug-build'; import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes'; import { handleCallbackErrors } from '../utils/handleCallbackErrors'; import { hasTracingEnabled } from '../utils/hasTracingEnabled'; @@ -212,6 +213,30 @@ export function suppressTracing(callback: () => T): T { }); } +/** + * Starts a new trace for the duration of the provided callback. Spans started within the + * callback will be part of the new trace instead of a potentially previously started trace. + * + * Important: Only use this function if you want to override the default trace lifetime and + * propagation mechanism of the SDK for the duration and scope of the provided callback. + * The newly created trace will also be the root of a new distributed trace, for example if + * you make http requests within the callback. + * This function might be useful if the operation you want to instrument should not be part + * of a potentially ongoing trace. + * + * Default behavior: + * - Server-side: A new trace is started for each incoming request. + * - Browser: A new trace is started for each page our route. Navigating to a new route + * or page will automatically create a new trace. + */ +export function startNewTrace(callback: () => T): T { + return withScope(scope => { + scope.setPropagationContext(generatePropagationContext()); + DEBUG_BUILD && logger.info(`Starting a new trace with id ${scope.getPropagationContext().traceId}`); + return withActiveSpan(null, callback); + }); +} + function createChildOrRootSpan({ parentSpan, spanContext, diff --git a/packages/core/test/lib/feedback.test.ts b/packages/core/test/lib/feedback.test.ts index faa17a8c51ea..e67521bfa2f8 100644 --- a/packages/core/test/lib/feedback.test.ts +++ b/packages/core/test/lib/feedback.test.ts @@ -1,5 +1,13 @@ import type { Span } from '@sentry/types'; -import { addBreadcrumb, getCurrentScope, setCurrentClient, startSpan, withIsolationScope, withScope } from '../../src'; +import { + Scope, + addBreadcrumb, + getCurrentScope, + setCurrentClient, + startSpan, + withIsolationScope, + withScope, +} from '../../src'; import { captureFeedback } from '../../src/feedback'; import { TestClient, getDefaultTestClientOptions } from '../mocks/client'; @@ -448,4 +456,45 @@ describe('captureFeedback', () => { ], ]); }); + + test('it allows to pass a custom client', async () => { + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://dsn@ingest.f00.f00/1', + enableSend: true, + }), + ); + setCurrentClient(client); + client.init(); + + const client2 = new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://dsn@ingest.f00.f00/1', + enableSend: true, + defaultIntegrations: false, + }), + ); + client2.init(); + const scope = new Scope(); + scope.setClient(client2); + + const mockTransport = jest.spyOn(client.getTransport()!, 'send'); + const mockTransport2 = jest.spyOn(client2.getTransport()!, 'send'); + + const eventId = captureFeedback( + { + message: 'test', + }, + {}, + scope, + ); + + await client.flush(); + await client2.flush(); + + expect(typeof eventId).toBe('string'); + + expect(mockTransport).not.toHaveBeenCalled(); + expect(mockTransport2).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index f2aa8460dba4..9399aa6b5a57 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -24,6 +24,7 @@ import { withActiveSpan, } from '../../../src/tracing'; import { SentryNonRecordingSpan } from '../../../src/tracing/sentryNonRecordingSpan'; +import { startNewTrace } from '../../../src/tracing/trace'; import { _setSpanForScope } from '../../../src/utils/spanOnScope'; import { getActiveSpan, getRootSpan, getSpanDescendants, spanIsSampled } from '../../../src/utils/spanUtils'; import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; @@ -1590,3 +1591,32 @@ describe('suppressTracing', () => { }); }); }); + +describe('startNewTrace', () => { + beforeEach(() => { + getCurrentScope().clear(); + getIsolationScope().clear(); + }); + + it('creates a new propagation context on the current scope', () => { + const oldCurrentScopeItraceId = getCurrentScope().getPropagationContext().traceId; + + startNewTrace(() => { + const newCurrentScopeItraceId = getCurrentScope().getPropagationContext().traceId; + + expect(newCurrentScopeItraceId).toMatch(/^[a-f0-9]{32}$/); + expect(newCurrentScopeItraceId).not.toEqual(oldCurrentScopeItraceId); + }); + }); + + it('keeps the propagation context on the isolation scope as-is', () => { + const oldIsolationScopeTraceId = getIsolationScope().getPropagationContext().traceId; + + startNewTrace(() => { + const newIsolationScopeTraceId = getIsolationScope().getPropagationContext().traceId; + + expect(newIsolationScopeTraceId).toMatch(/^[a-f0-9]{32}$/); + expect(newIsolationScopeTraceId).toEqual(oldIsolationScopeTraceId); + }); + }); +}); diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index 1857de352798..aa30c762d624 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -58,6 +58,7 @@ export { startSpan, startInactiveSpan, startSpanManual, + startNewTrace, metricsDefault as metrics, inboundFiltersIntegration, linkedErrorsIntegration, diff --git a/packages/feedback/rollup.bundle.config.mjs b/packages/feedback/rollup.bundle.config.mjs index 4d5620c4c040..f22f4fc9c647 100644 --- a/packages/feedback/rollup.bundle.config.mjs +++ b/packages/feedback/rollup.bundle.config.mjs @@ -9,8 +9,10 @@ export default [ licenseTitle: '@sentry-internal/feedback', outputFileBase: () => 'bundles/feedback', sucrase: { + // The feedback widget is using preact so we need different pragmas and jsx runtimes jsxPragma: 'h', jsxFragmentPragma: 'Fragment', + jsxRuntime: 'classic', }, }), ), @@ -22,8 +24,10 @@ export default [ licenseTitle: '@sentry-internal/feedback', outputFileBase: () => 'bundles/feedback-screenshot', sucrase: { + // The feedback widget is using preact so we need different pragmas and jsx runtimes jsxPragma: 'h', jsxFragmentPragma: 'Fragment', + jsxRuntime: 'classic', }, }), ), @@ -35,8 +39,10 @@ export default [ licenseTitle: '@sentry-internal/feedback', outputFileBase: () => 'bundles/feedback-modal', sucrase: { + // The feedback widget is using preact so we need different pragmas and jsx runtimes jsxPragma: 'h', jsxFragmentPragma: 'Fragment', + jsxRuntime: 'classic', }, }), ), diff --git a/packages/feedback/rollup.npm.config.mjs b/packages/feedback/rollup.npm.config.mjs index cdc5fcf7ad1c..2b4f6af2739c 100644 --- a/packages/feedback/rollup.npm.config.mjs +++ b/packages/feedback/rollup.npm.config.mjs @@ -16,8 +16,10 @@ export default makeNPMConfigVariants( }, }, sucrase: { + // The feedback widget is using preact so we need different pragmas and jsx runtimes jsxPragma: 'h', jsxFragmentPragma: 'Fragment', + jsxRuntime: 'classic', }, }), ); diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 067c27818a10..6affee429e1f 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -62,6 +62,7 @@ export { startSpan, startInactiveSpan, startSpanManual, + startNewTrace, withActiveSpan, getRootSpan, getSpanDescendants, diff --git a/packages/integration-shims/src/index.ts b/packages/integration-shims/src/index.ts index 510b26ddbb76..616f9910cf35 100644 --- a/packages/integration-shims/src/index.ts +++ b/packages/integration-shims/src/index.ts @@ -1,3 +1,4 @@ export { feedbackIntegrationShim } from './Feedback'; export { replayIntegrationShim } from './Replay'; export { browserTracingIntegrationShim } from './BrowserTracing'; +export { metricsShim } from './metrics'; diff --git a/packages/integration-shims/src/metrics.ts b/packages/integration-shims/src/metrics.ts new file mode 100644 index 000000000000..9af425e0f629 --- /dev/null +++ b/packages/integration-shims/src/metrics.ts @@ -0,0 +1,16 @@ +import type { Metrics } from '@sentry/types'; +import { consoleSandbox } from '@sentry/utils'; + +function warn(): void { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('You are using metrics even though this bundle does not include tracing.'); + }); +} + +export const metricsShim: Metrics = { + increment: warn, + distribution: warn, + set: warn, + gauge: warn, +}; diff --git a/packages/nextjs/src/common/_error.ts b/packages/nextjs/src/common/_error.ts index f79c844adba7..f3a198919a72 100644 --- a/packages/nextjs/src/common/_error.ts +++ b/packages/nextjs/src/common/_error.ts @@ -1,6 +1,7 @@ import { captureException, withScope } from '@sentry/core'; import type { NextPageContext } from 'next'; -import { flushQueue } from './utils/responseEnd'; +import { flushSafelyWithTimeout } from './utils/responseEnd'; +import { vercelWaitUntil } from './utils/vercelWaitUntil'; type ContextOrProps = { req?: NextPageContext['req']; @@ -53,7 +54,5 @@ export async function captureUnderscoreErrorException(contextOrProps: ContextOrP }); }); - // In case this is being run as part of a serverless function (as is the case with the server half of nextjs apps - // deployed to vercel), make sure the error gets sent to Sentry before the lambda exits. - await flushQueue(); + vercelWaitUntil(flushSafelyWithTimeout()); } diff --git a/packages/nextjs/src/common/types.ts b/packages/nextjs/src/common/types.ts index c182e80c2d20..05d89d3e3159 100644 --- a/packages/nextjs/src/common/types.ts +++ b/packages/nextjs/src/common/types.ts @@ -48,11 +48,6 @@ export type VercelCronsConfig = { path?: string; schedule?: string }[] | undefin export type NextApiHandler = { (req: NextApiRequest, res: NextApiResponse): void | Promise | unknown | Promise; __sentry_route__?: string; - - /** - * A property we set in our integration tests to simulate running an API route on platforms that don't support streaming. - */ - __sentry_test_doesnt_support_streaming__?: true; }; export type WrappedNextApiHandler = { diff --git a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts index 9324d59829c1..65bdabc93dda 100644 --- a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts +++ b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts @@ -12,8 +12,9 @@ import { import { winterCGRequestToRequestData } from '@sentry/utils'; import type { EdgeRouteHandler } from '../../edge/types'; -import { flushQueue } from './responseEnd'; +import { flushSafelyWithTimeout } from './responseEnd'; import { commonObjectToIsolationScope, escapeNextjsTracing } from './tracingUtils'; +import { vercelWaitUntil } from './vercelWaitUntil'; /** * Wraps a function on the edge runtime with error and performance monitoring. @@ -80,9 +81,11 @@ export function withEdgeWrapping( return handlerResult; }, - ).finally(() => flushQueue()); + ); }, - ); + ).finally(() => { + vercelWaitUntil(flushSafelyWithTimeout()); + }); }); }); }; diff --git a/packages/nextjs/src/common/utils/platformSupportsStreaming.ts b/packages/nextjs/src/common/utils/platformSupportsStreaming.ts deleted file mode 100644 index 39b19f0ab8db..000000000000 --- a/packages/nextjs/src/common/utils/platformSupportsStreaming.ts +++ /dev/null @@ -1 +0,0 @@ -export const platformSupportsStreaming = (): boolean => !process.env.LAMBDA_TASK_ROOT && !process.env.VERCEL; diff --git a/packages/nextjs/src/common/utils/responseEnd.ts b/packages/nextjs/src/common/utils/responseEnd.ts index c287933f6c39..e5aedd9be773 100644 --- a/packages/nextjs/src/common/utils/responseEnd.ts +++ b/packages/nextjs/src/common/utils/responseEnd.ts @@ -44,10 +44,14 @@ export function finishSpan(span: Span, res: ServerResponse): void { span.end(); } -/** Flush the event queue to ensure that events get sent to Sentry before the response is finished and the lambda ends */ -export async function flushQueue(): Promise { +/** + * Flushes pending Sentry events with a 2 second timeout and in a way that cannot create unhandled promise rejections. + */ +export async function flushSafelyWithTimeout(): Promise { try { DEBUG_BUILD && logger.log('Flushing events...'); + // We give things that are currently stuck in event processors a tiny bit more time to finish before flushing. 50ms was chosen very unscientifically. + await new Promise(resolve => setTimeout(resolve, 50)); await flush(2000); DEBUG_BUILD && logger.log('Done flushing events'); } catch (e) { diff --git a/packages/nextjs/src/common/utils/vercelWaitUntil.ts b/packages/nextjs/src/common/utils/vercelWaitUntil.ts new file mode 100644 index 000000000000..15c6015fe4c9 --- /dev/null +++ b/packages/nextjs/src/common/utils/vercelWaitUntil.ts @@ -0,0 +1,21 @@ +import { GLOBAL_OBJ } from '@sentry/utils'; + +interface VercelRequestContextGlobal { + get?(): { + waitUntil?: (task: Promise) => void; + }; +} + +/** + * Function that delays closing of a Vercel lambda until the provided promise is resolved. + * + * Vendored from https://www.npmjs.com/package/@vercel/functions + */ +export function vercelWaitUntil(task: Promise): void { + const vercelRequestContextGlobal: VercelRequestContextGlobal | undefined = + // @ts-expect-error This is not typed + GLOBAL_OBJ[Symbol.for('@vercel/request-context')]; + + const ctx = vercelRequestContextGlobal?.get?.() ?? {}; + ctx.waitUntil?.(task); +} diff --git a/packages/nextjs/src/common/utils/wrapperUtils.ts b/packages/nextjs/src/common/utils/wrapperUtils.ts index d1d1cd961b3f..306bc96e30f6 100644 --- a/packages/nextjs/src/common/utils/wrapperUtils.ts +++ b/packages/nextjs/src/common/utils/wrapperUtils.ts @@ -15,9 +15,9 @@ import { import type { Span } from '@sentry/types'; import { isString } from '@sentry/utils'; -import { platformSupportsStreaming } from './platformSupportsStreaming'; -import { autoEndSpanOnResponseEnd, flushQueue } from './responseEnd'; +import { autoEndSpanOnResponseEnd, flushSafelyWithTimeout } from './responseEnd'; import { commonObjectToIsolationScope, escapeNextjsTracing } from './tracingUtils'; +import { vercelWaitUntil } from './vercelWaitUntil'; declare module 'http' { interface IncomingMessage { @@ -124,15 +124,14 @@ export function withTracedServerSideDataFetcher Pr throw e; } finally { dataFetcherSpan.end(); - if (!platformSupportsStreaming()) { - await flushQueue(); - } } }, ); }); }); }); + }).finally(() => { + vercelWaitUntil(flushSafelyWithTimeout()); }); }; } @@ -198,10 +197,9 @@ export async function callDataFetcherTraced Promis throw e; } finally { dataFetcherSpan.end(); - if (!platformSupportsStreaming()) { - await flushQueue(); - } } }, - ); + ).finally(() => { + vercelWaitUntil(flushSafelyWithTimeout()); + }); } diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index 109743eea01a..14c701638ee5 100644 --- a/packages/nextjs/src/common/withServerActionInstrumentation.ts +++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts @@ -9,9 +9,9 @@ import { logger } from '@sentry/utils'; import { DEBUG_BUILD } from './debug-build'; import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils'; -import { platformSupportsStreaming } from './utils/platformSupportsStreaming'; -import { flushQueue } from './utils/responseEnd'; +import { flushSafelyWithTimeout } from './utils/responseEnd'; import { escapeNextjsTracing } from './utils/tracingUtils'; +import { vercelWaitUntil } from './utils/vercelWaitUntil'; interface Options { formData?: FormData; @@ -131,16 +131,7 @@ async function withServerActionInstrumentationImplementation { - target.apply(thisArg, argArray); - }); - } + vercelWaitUntil(flushSafelyWithTimeout()); + target.apply(thisArg, argArray); }, }); @@ -138,14 +131,7 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz setHttpStatus(span, res.statusCode); span.end(); - // Make sure we have a chance to finish the transaction and flush events to Sentry before the handler errors - // out. (Apps which are deployed on Vercel run their API routes in lambdas, and those lambdas will shut down the - // moment they detect an error, so it's important to get this done before rethrowing the error. Apps not - // deployed serverlessly will run into this cleanup code again in `res.end(), but the transaction will already - // be finished and the queue will already be empty, so effectively it'll just no-op.) - if (platformSupportsStreaming() && !wrappingTarget.__sentry_test_doesnt_support_streaming__) { - await flushQueue(); - } + vercelWaitUntil(flushSafelyWithTimeout()); // We rethrow here so that nextjs can do with the error whatever it would normally do. (Sometimes "whatever it // would normally do" is to allow the error to bubble up to the global handlers - another reason we need to mark diff --git a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts index be378dc8cd5e..e55eedd9802e 100644 --- a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts @@ -13,13 +13,13 @@ import { import { propagationContextFromHeaders, winterCGHeadersToDict } from '@sentry/utils'; import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils'; import type { RouteHandlerContext } from './types'; -import { platformSupportsStreaming } from './utils/platformSupportsStreaming'; -import { flushQueue } from './utils/responseEnd'; +import { flushSafelyWithTimeout } from './utils/responseEnd'; import { commonObjectToIsolationScope, commonObjectToPropagationContext, escapeNextjsTracing, } from './utils/tracingUtils'; +import { vercelWaitUntil } from './utils/vercelWaitUntil'; /** * Wraps a Next.js route handler with performance and error instrumentation. @@ -97,11 +97,7 @@ export function wrapRouteHandlerWithSentry any>( }, ); } finally { - if (!platformSupportsStreaming() || process.env.NEXT_RUNTIME === 'edge') { - // 1. Edge transport requires manual flushing - // 2. Lambdas require manual flushing to prevent execution freeze before the event is sent - await flushQueue(); - } + vercelWaitUntil(flushSafelyWithTimeout()); } }); }); diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index fe185679528d..0d1e224bdf47 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -14,12 +14,13 @@ import { propagationContextFromHeaders, uuid4, winterCGHeadersToDict } from '@se import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/nextNavigationErrorUtils'; import type { ServerComponentContext } from '../common/types'; -import { flushQueue } from './utils/responseEnd'; +import { flushSafelyWithTimeout } from './utils/responseEnd'; import { commonObjectToIsolationScope, commonObjectToPropagationContext, escapeNextjsTracing, } from './utils/tracingUtils'; +import { vercelWaitUntil } from './utils/vercelWaitUntil'; /** * Wraps an `app` directory server component with Sentry error instrumentation. @@ -93,10 +94,7 @@ export function wrapServerComponentWithSentry any> }, () => { span.end(); - - // flushQueue should not throw - // eslint-disable-next-line @typescript-eslint/no-floating-promises - flushQueue(); + vercelWaitUntil(flushSafelyWithTimeout()); }, ); }, diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index acfa4f03af97..2b5d7251186a 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -1,4 +1,4 @@ -import { addEventProcessor, applySdkMetadata, getClient } from '@sentry/core'; +import { applySdkMetadata, getClient, getGlobalScope } from '@sentry/core'; import { getDefaultIntegrations, init as nodeInit } from '@sentry/node'; import type { NodeOptions } from '@sentry/node'; import { GLOBAL_OBJ, logger } from '@sentry/utils'; @@ -143,7 +143,7 @@ export function init(options: NodeOptions): void { } }); - addEventProcessor( + getGlobalScope().addEventProcessor( Object.assign( (event => { if (event.type === 'transaction') { @@ -181,8 +181,33 @@ export function init(options: NodeOptions): void { ), ); + getGlobalScope().addEventProcessor( + Object.assign( + ((event, hint) => { + if (event.type !== undefined) { + return event; + } + + const originalException = hint.originalException; + + const isReactControlFlowError = + typeof originalException === 'object' && + originalException !== null && + '$$typeof' in originalException && + originalException.$$typeof === Symbol.for('react.postpone'); + + if (isReactControlFlowError) { + return null; + } + + return event; + }) satisfies EventProcessor, + { id: 'DropReactControlFlowErrors' }, + ), + ); + if (process.env.NODE_ENV === 'development') { - addEventProcessor(devErrorSymbolicationEventProcessor); + getGlobalScope().addEventProcessor(devErrorSymbolicationEventProcessor); } DEBUG_BUILD && logger.log('SDK successfully initialized'); diff --git a/packages/nextjs/test/integration/pages/api/doubleEndMethodOnVercel.ts b/packages/nextjs/test/integration/pages/api/doubleEndMethodOnVercel.ts index f32fcf55fafd..b0cfca8651be 100644 --- a/packages/nextjs/test/integration/pages/api/doubleEndMethodOnVercel.ts +++ b/packages/nextjs/test/integration/pages/api/doubleEndMethodOnVercel.ts @@ -6,6 +6,4 @@ const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise { - const _breadcrumbs = typeof options.breadcrumbs === 'undefined' ? true : options.breadcrumbs; - const _ignoreOutgoingRequests = options.ignoreOutgoingRequests; - const _ignoreIncomingRequests = options.ignoreIncomingRequests; - const _InstrumentationClass = options._instrumentation || HttpInstrumentation; +let _httpOptions: HttpOptions = {}; +let _httpInstrumentation: HttpInstrumentation | undefined; + +/** + * Instrument the HTTP module. + * This can only be instrumented once! If this called again later, we just update the options. + */ +export const instrumentHttp = Object.assign( + function (): void { + if (_httpInstrumentation) { + return; + } + + const _InstrumentationClass = _httpOptions._instrumentation || HttpInstrumentation; + + _httpInstrumentation = new _InstrumentationClass({ + ignoreOutgoingRequestHook: request => { + const url = getRequestUrl(request); + + if (!url) { + return false; + } + + if (isSentryRequestUrl(url, getClient())) { + return true; + } + + const _ignoreOutgoingRequests = _httpOptions.ignoreOutgoingRequests; + if (_ignoreOutgoingRequests && _ignoreOutgoingRequests(url)) { + return true; + } + + return false; + }, + + ignoreIncomingRequestHook: request => { + const url = getRequestUrl(request); + + const method = request.method?.toUpperCase(); + // We do not capture OPTIONS/HEAD requests as transactions + if (method === 'OPTIONS' || method === 'HEAD') { + return true; + } + + const _ignoreIncomingRequests = _httpOptions.ignoreIncomingRequests; + if (_ignoreIncomingRequests && _ignoreIncomingRequests(url)) { + return true; + } + return false; + }, + + requireParentforOutgoingSpans: false, + requireParentforIncomingSpans: false, + requestHook: (span, req) => { + addOriginToSpan(span, 'auto.http.otel.http'); + + // both, incoming requests and "client" requests made within the app trigger the requestHook + // we only want to isolate and further annotate incoming requests (IncomingMessage) + if (_isClientRequest(req)) { + return; + } + + const scopes = getCapturedScopesOnSpan(span); + + const isolationScope = (scopes.isolationScope || getIsolationScope()).clone(); + const scope = scopes.scope || getCurrentScope(); + + // Update the isolation scope, isolate this request + isolationScope.setSDKProcessingMetadata({ request: req }); + + const client = getClient(); + if (client && client.getOptions().autoSessionTracking) { + isolationScope.setRequestSession({ status: 'ok' }); + } + setIsolationScope(isolationScope); + setCapturedScopesOnSpan(span, scope, isolationScope); + + // attempt to update the scope's `transactionName` based on the request URL + // Ideally, framework instrumentations coming after the HttpInstrumentation + // update the transactionName once we get a parameterized route. + const httpMethod = (req.method || 'GET').toUpperCase(); + const httpTarget = stripUrlQueryAndFragment(req.url || '/'); + + const bestEffortTransactionName = `${httpMethod} ${httpTarget}`; + + isolationScope.setTransactionName(bestEffortTransactionName); + }, + responseHook: () => { + const client = getClient(); + if (client && client.getOptions().autoSessionTracking) { + setImmediate(() => { + client['_captureRequestSession'](); + }); + } + }, + applyCustomAttributesOnSpan: ( + _span: Span, + request: ClientRequest | HTTPModuleRequestIncomingMessage, + response: HTTPModuleRequestIncomingMessage | ServerResponse, + ) => { + const _breadcrumbs = typeof _httpOptions.breadcrumbs === 'undefined' ? true : _httpOptions.breadcrumbs; + if (_breadcrumbs) { + _addRequestBreadcrumb(request, response); + } + }, + }); + + addOpenTelemetryInstrumentation(_httpInstrumentation); + }, + { + id: INTEGRATION_NAME, + }, +); + +const _httpIntegration = ((options: HttpOptions = {}) => { return { - name: 'Http', + name: INTEGRATION_NAME, setupOnce() { - addOpenTelemetryInstrumentation( - new _InstrumentationClass({ - ignoreOutgoingRequestHook: request => { - const url = getRequestUrl(request); - - if (!url) { - return false; - } - - if (isSentryRequestUrl(url, getClient())) { - return true; - } - - if (_ignoreOutgoingRequests && _ignoreOutgoingRequests(url)) { - return true; - } - - return false; - }, - - ignoreIncomingRequestHook: request => { - const url = getRequestUrl(request); - - const method = request.method?.toUpperCase(); - // We do not capture OPTIONS/HEAD requests as transactions - if (method === 'OPTIONS' || method === 'HEAD') { - return true; - } - - if (_ignoreIncomingRequests && _ignoreIncomingRequests(url)) { - return true; - } - - return false; - }, - - requireParentforOutgoingSpans: false, - requireParentforIncomingSpans: false, - requestHook: (span, req) => { - addOriginToSpan(span, 'auto.http.otel.http'); - - // both, incoming requests and "client" requests made within the app trigger the requestHook - // we only want to isolate and further annotate incoming requests (IncomingMessage) - if (_isClientRequest(req)) { - return; - } - - const scopes = getCapturedScopesOnSpan(span); - - const isolationScope = (scopes.isolationScope || getIsolationScope()).clone(); - const scope = scopes.scope || getCurrentScope(); - - // Update the isolation scope, isolate this request - isolationScope.setSDKProcessingMetadata({ request: req }); - - const client = getClient(); - if (client && client.getOptions().autoSessionTracking) { - isolationScope.setRequestSession({ status: 'ok' }); - } - setIsolationScope(isolationScope); - setCapturedScopesOnSpan(span, scope, isolationScope); - - // attempt to update the scope's `transactionName` based on the request URL - // Ideally, framework instrumentations coming after the HttpInstrumentation - // update the transactionName once we get a parameterized route. - const httpMethod = (req.method || 'GET').toUpperCase(); - const httpTarget = stripUrlQueryAndFragment(req.url || '/'); - - const bestEffortTransactionName = `${httpMethod} ${httpTarget}`; - - isolationScope.setTransactionName(bestEffortTransactionName); - }, - responseHook: () => { - const client = getClient(); - if (client && client.getOptions().autoSessionTracking) { - setImmediate(() => { - client['_captureRequestSession'](); - }); - } - }, - applyCustomAttributesOnSpan: ( - _span: Span, - request: ClientRequest | HTTPModuleRequestIncomingMessage, - response: HTTPModuleRequestIncomingMessage | ServerResponse, - ) => { - if (_breadcrumbs) { - _addRequestBreadcrumb(request, response); - } - }, - }), - ); + _httpOptions = options; + instrumentHttp(); }, }; }) satisfies IntegrationFn; diff --git a/packages/node/src/integrations/local-variables/local-variables-async.ts b/packages/node/src/integrations/local-variables/local-variables-async.ts index 944ccca758d9..06056aae6a39 100644 --- a/packages/node/src/integrations/local-variables/local-variables-async.ts +++ b/packages/node/src/integrations/local-variables/local-variables-async.ts @@ -75,7 +75,7 @@ export const localVariablesAsyncIntegration = defineIntegration((( async function startInspector(): Promise { // We load inspector dynamically because on some platforms Node is built without inspector support - const inspector = await import('inspector'); + const inspector = await import('node:inspector'); if (!inspector.url()) { inspector.open(0); } diff --git a/packages/node/src/integrations/local-variables/local-variables-sync.ts b/packages/node/src/integrations/local-variables/local-variables-sync.ts index 66e8eca4d32f..9616e534625a 100644 --- a/packages/node/src/integrations/local-variables/local-variables-sync.ts +++ b/packages/node/src/integrations/local-variables/local-variables-sync.ts @@ -1,5 +1,4 @@ -import type { Debugger, InspectorNotification, Runtime } from 'node:inspector'; -import { Session } from 'node:inspector'; +import type { Debugger, InspectorNotification, Runtime, Session } from 'node:inspector'; import { defineIntegration, getClient } from '@sentry/core'; import type { Event, Exception, IntegrationFn, StackParser } from '@sentry/types'; import { LRUMap, logger } from '@sentry/utils'; @@ -75,11 +74,18 @@ export function createCallbackList(complete: Next): CallbackWrapper { * https://nodejs.org/docs/latest-v14.x/api/inspector.html */ class AsyncSession implements DebugSession { - private readonly _session: Session; - /** Throws if inspector API is not available */ - public constructor() { - this._session = new Session(); + private constructor(private readonly _session: Session) { + // + } + + public static async create(orDefault?: DebugSession | undefined): Promise { + if (orDefault) { + return orDefault; + } + + const inspector = await import('node:inspector'); + return new AsyncSession(new inspector.Session()); } /** @inheritdoc */ @@ -194,18 +200,6 @@ class AsyncSession implements DebugSession { } } -/** - * When using Vercel pkg, the inspector module is not available. - * https://github.com/getsentry/sentry-javascript/issues/6769 - */ -function tryNewAsyncSession(): AsyncSession | undefined { - try { - return new AsyncSession(); - } catch (e) { - return undefined; - } -} - const INTEGRATION_NAME = 'LocalVariables'; /** @@ -213,66 +207,12 @@ const INTEGRATION_NAME = 'LocalVariables'; */ const _localVariablesSyncIntegration = (( options: LocalVariablesIntegrationOptions = {}, - session: DebugSession | undefined = tryNewAsyncSession(), + sessionOverride?: DebugSession, ) => { const cachedFrames: LRUMap = new LRUMap(20); let rateLimiter: RateLimitIncrement | undefined; let shouldProcessEvent = false; - function handlePaused( - stackParser: StackParser, - { params: { reason, data, callFrames } }: InspectorNotification, - complete: () => void, - ): void { - if (reason !== 'exception' && reason !== 'promiseRejection') { - complete(); - return; - } - - rateLimiter?.(); - - // data.description contains the original error.stack - const exceptionHash = hashFromStack(stackParser, data?.description); - - if (exceptionHash == undefined) { - complete(); - return; - } - - const { add, next } = createCallbackList(frames => { - cachedFrames.set(exceptionHash, frames); - complete(); - }); - - // Because we're queuing up and making all these calls synchronously, we can potentially overflow the stack - // For this reason we only attempt to get local variables for the first 5 frames - for (let i = 0; i < Math.min(callFrames.length, 5); i++) { - const { scopeChain, functionName, this: obj } = callFrames[i]; - - const localScope = scopeChain.find(scope => scope.type === 'local'); - - // obj.className is undefined in ESM modules - const fn = obj.className === 'global' || !obj.className ? functionName : `${obj.className}.${functionName}`; - - if (localScope?.object.objectId === undefined) { - add(frames => { - frames[i] = { function: fn }; - next(frames); - }); - } else { - const id = localScope.object.objectId; - add(frames => - session?.getLocalVariables(id, vars => { - frames[i] = { function: fn, vars }; - next(frames); - }), - ); - } - } - - next([]); - } - function addLocalVariablesToException(exception: Exception): void { const hash = hashFrames(exception?.stacktrace?.frames); @@ -330,44 +270,108 @@ const _localVariablesSyncIntegration = (( const client = getClient(); const clientOptions = client?.getOptions(); - if (session && clientOptions?.includeLocalVariables) { - // Only setup this integration if the Node version is >= v18 - // https://github.com/getsentry/sentry-javascript/issues/7697 - const unsupportedNodeVersion = NODE_MAJOR < 18; + if (!clientOptions?.includeLocalVariables) { + return; + } - if (unsupportedNodeVersion) { - logger.log('The `LocalVariables` integration is only supported on Node >= v18.'); - return; - } + // Only setup this integration if the Node version is >= v18 + // https://github.com/getsentry/sentry-javascript/issues/7697 + const unsupportedNodeVersion = NODE_MAJOR < 18; - const captureAll = options.captureAllExceptions !== false; - - session.configureAndConnect( - (ev, complete) => - handlePaused(clientOptions.stackParser, ev as InspectorNotification, complete), - captureAll, - ); - - if (captureAll) { - const max = options.maxExceptionsPerSecond || 50; - - rateLimiter = createRateLimiter( - max, - () => { - logger.log('Local variables rate-limit lifted.'); - session?.setPauseOnExceptions(true); - }, - seconds => { - logger.log( - `Local variables rate-limit exceeded. Disabling capturing of caught exceptions for ${seconds} seconds.`, - ); - session?.setPauseOnExceptions(false); - }, + if (unsupportedNodeVersion) { + logger.log('The `LocalVariables` integration is only supported on Node >= v18.'); + return; + } + + AsyncSession.create(sessionOverride).then( + session => { + function handlePaused( + stackParser: StackParser, + { params: { reason, data, callFrames } }: InspectorNotification, + complete: () => void, + ): void { + if (reason !== 'exception' && reason !== 'promiseRejection') { + complete(); + return; + } + + rateLimiter?.(); + + // data.description contains the original error.stack + const exceptionHash = hashFromStack(stackParser, data?.description); + + if (exceptionHash == undefined) { + complete(); + return; + } + + const { add, next } = createCallbackList(frames => { + cachedFrames.set(exceptionHash, frames); + complete(); + }); + + // Because we're queuing up and making all these calls synchronously, we can potentially overflow the stack + // For this reason we only attempt to get local variables for the first 5 frames + for (let i = 0; i < Math.min(callFrames.length, 5); i++) { + const { scopeChain, functionName, this: obj } = callFrames[i]; + + const localScope = scopeChain.find(scope => scope.type === 'local'); + + // obj.className is undefined in ESM modules + const fn = + obj.className === 'global' || !obj.className ? functionName : `${obj.className}.${functionName}`; + + if (localScope?.object.objectId === undefined) { + add(frames => { + frames[i] = { function: fn }; + next(frames); + }); + } else { + const id = localScope.object.objectId; + add(frames => + session?.getLocalVariables(id, vars => { + frames[i] = { function: fn, vars }; + next(frames); + }), + ); + } + } + + next([]); + } + + const captureAll = options.captureAllExceptions !== false; + + session.configureAndConnect( + (ev, complete) => + handlePaused(clientOptions.stackParser, ev as InspectorNotification, complete), + captureAll, ); - } - shouldProcessEvent = true; - } + if (captureAll) { + const max = options.maxExceptionsPerSecond || 50; + + rateLimiter = createRateLimiter( + max, + () => { + logger.log('Local variables rate-limit lifted.'); + session?.setPauseOnExceptions(true); + }, + seconds => { + logger.log( + `Local variables rate-limit exceeded. Disabling capturing of caught exceptions for ${seconds} seconds.`, + ); + session?.setPauseOnExceptions(false); + }, + ); + } + + shouldProcessEvent = true; + }, + error => { + logger.log('The `LocalVariables` integration failed to start.', error); + }, + ); }, processEvent(event: Event): Event { if (shouldProcessEvent) { diff --git a/packages/node/src/integrations/tracing/connect.ts b/packages/node/src/integrations/tracing/connect.ts index 7d3e5a28137f..5ea6011c5257 100644 --- a/packages/node/src/integrations/tracing/connect.ts +++ b/packages/node/src/integrations/tracing/connect.ts @@ -7,8 +7,8 @@ import { getClient, spanToJSON, } from '@sentry/core'; -import { addOpenTelemetryInstrumentation } from '@sentry/opentelemetry'; import type { IntegrationFn, Span } from '@sentry/types'; +import { generateInstrumentOnce } from '../../otel/instrument'; import { ensureIsWrapped } from '../../utils/ensureIsWrapped'; type ConnectApp = { @@ -16,11 +16,15 @@ type ConnectApp = { use: (middleware: any) => void; }; +const INTEGRATION_NAME = 'Connect'; + +export const instrumentConnect = generateInstrumentOnce(INTEGRATION_NAME, () => new ConnectInstrumentation()); + const _connectIntegration = (() => { return { - name: 'Connect', + name: INTEGRATION_NAME, setupOnce() { - addOpenTelemetryInstrumentation(new ConnectInstrumentation({})); + instrumentConnect(); }, }; }) satisfies IntegrationFn; diff --git a/packages/node/src/integrations/tracing/express.ts b/packages/node/src/integrations/tracing/express.ts index cddb9bb7e0e5..00c5735207d4 100644 --- a/packages/node/src/integrations/tracing/express.ts +++ b/packages/node/src/integrations/tracing/express.ts @@ -2,53 +2,59 @@ import type * as http from 'node:http'; import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, defineIntegration, getDefaultIsolationScope, spanToJSON } from '@sentry/core'; import { captureException, getClient, getIsolationScope } from '@sentry/core'; -import { addOpenTelemetryInstrumentation } from '@sentry/opentelemetry'; import type { IntegrationFn } from '@sentry/types'; import { logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../../debug-build'; +import { generateInstrumentOnce } from '../../otel/instrument'; import type { NodeClient } from '../../sdk/client'; import { addOriginToSpan } from '../../utils/addOriginToSpan'; import { ensureIsWrapped } from '../../utils/ensureIsWrapped'; +const INTEGRATION_NAME = 'Express'; + +export const instrumentExpress = generateInstrumentOnce( + INTEGRATION_NAME, + () => + new ExpressInstrumentation({ + requestHook(span) { + addOriginToSpan(span, 'auto.http.otel.express'); + + const attributes = spanToJSON(span).data || {}; + // this is one of: middleware, request_handler, router + const type = attributes['express.type']; + + if (type) { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, `${type}.express`); + } + + // Also update the name, we don't need to "middleware - " prefix + const name = attributes['express.name']; + if (typeof name === 'string') { + span.updateName(name); + } + }, + spanNameHook(info, defaultName) { + if (getIsolationScope() === getDefaultIsolationScope()) { + DEBUG_BUILD && + logger.warn('Isolation scope is still default isolation scope - skipping setting transactionName'); + return defaultName; + } + if (info.layerType === 'request_handler') { + // type cast b/c Otel unfortunately types info.request as any :( + const req = info.request as { method?: string }; + const method = req.method ? req.method.toUpperCase() : 'GET'; + getIsolationScope().setTransactionName(`${method} ${info.route}`); + } + return defaultName; + }, + }), +); + const _expressIntegration = (() => { return { - name: 'Express', + name: INTEGRATION_NAME, setupOnce() { - addOpenTelemetryInstrumentation( - new ExpressInstrumentation({ - requestHook(span) { - addOriginToSpan(span, 'auto.http.otel.express'); - - const attributes = spanToJSON(span).data || {}; - // this is one of: middleware, request_handler, router - const type = attributes['express.type']; - - if (type) { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, `${type}.express`); - } - - // Also update the name, we don't need to "middleware - " prefix - const name = attributes['express.name']; - if (typeof name === 'string') { - span.updateName(name); - } - }, - spanNameHook(info, defaultName) { - if (getIsolationScope() === getDefaultIsolationScope()) { - DEBUG_BUILD && - logger.warn('Isolation scope is still default isolation scope - skipping setting transactionName'); - return defaultName; - } - if (info.layerType === 'request_handler') { - // type cast b/c Otel unfortunately types info.request as any :( - const req = info.request as { method?: string }; - const method = req.method ? req.method.toUpperCase() : 'GET'; - getIsolationScope().setTransactionName(`${method} ${info.route}`); - } - return defaultName; - }, - }), - ); + instrumentExpress(); }, }; }) satisfies IntegrationFn; diff --git a/packages/node/src/integrations/tracing/fastify.ts b/packages/node/src/integrations/tracing/fastify.ts index 6286bd8a0f97..27657d94d3d3 100644 --- a/packages/node/src/integrations/tracing/fastify.ts +++ b/packages/node/src/integrations/tracing/fastify.ts @@ -8,8 +8,8 @@ import { getIsolationScope, spanToJSON, } from '@sentry/core'; -import { addOpenTelemetryInstrumentation } from '@sentry/opentelemetry'; import type { IntegrationFn, Span } from '@sentry/types'; +import { generateInstrumentOnce } from '../../otel/instrument'; import { ensureIsWrapped } from '../../utils/ensureIsWrapped'; // We inline the types we care about here @@ -33,17 +33,23 @@ interface FastifyRequestRouteInfo { routerPath?: string; } +const INTEGRATION_NAME = 'Fastify'; + +export const instrumentFastify = generateInstrumentOnce( + INTEGRATION_NAME, + () => + new FastifyInstrumentation({ + requestHook(span) { + addFastifySpanAttributes(span); + }, + }), +); + const _fastifyIntegration = (() => { return { - name: 'Fastify', + name: INTEGRATION_NAME, setupOnce() { - addOpenTelemetryInstrumentation( - new FastifyInstrumentation({ - requestHook(span) { - addFastifySpanAttributes(span); - }, - }), - ); + instrumentFastify(); }, }; }) satisfies IntegrationFn; diff --git a/packages/node/src/integrations/tracing/graphql.ts b/packages/node/src/integrations/tracing/graphql.ts index 4f4fdc93dac9..097ee3ba43f8 100644 --- a/packages/node/src/integrations/tracing/graphql.ts +++ b/packages/node/src/integrations/tracing/graphql.ts @@ -1,7 +1,7 @@ import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql'; import { defineIntegration } from '@sentry/core'; -import { addOpenTelemetryInstrumentation } from '@sentry/opentelemetry'; import type { IntegrationFn } from '@sentry/types'; +import { generateInstrumentOnce } from '../../otel/instrument'; import { addOriginToSpan } from '../../utils/addOriginToSpan'; @@ -20,24 +20,31 @@ interface GraphqlOptions { ignoreTrivalResolveSpans?: boolean; } -const _graphqlIntegration = ((_options: GraphqlOptions = {}) => { - const options = { - ignoreResolveSpans: true, - ignoreTrivialResolveSpans: true, - ..._options, - }; +const INTEGRATION_NAME = 'Graphql'; + +export const instrumentGraphql = generateInstrumentOnce( + INTEGRATION_NAME, + (_options: GraphqlOptions = {}) => { + const options = { + ignoreResolveSpans: true, + ignoreTrivialResolveSpans: true, + ..._options, + }; + + return new GraphQLInstrumentation({ + ...options, + responseHook(span) { + addOriginToSpan(span, 'auto.graphql.otel.graphql'); + }, + }); + }, +); +const _graphqlIntegration = ((options: GraphqlOptions = {}) => { return { - name: 'Graphql', + name: INTEGRATION_NAME, setupOnce() { - addOpenTelemetryInstrumentation( - new GraphQLInstrumentation({ - ...options, - responseHook(span) { - addOriginToSpan(span, 'auto.graphql.otel.graphql'); - }, - }), - ); + instrumentGraphql(options); }, }; }) satisfies IntegrationFn; diff --git a/packages/node/src/integrations/tracing/hapi/index.ts b/packages/node/src/integrations/tracing/hapi/index.ts index ee03cfc34ac6..d197fbed0b2d 100644 --- a/packages/node/src/integrations/tracing/hapi/index.ts +++ b/packages/node/src/integrations/tracing/hapi/index.ts @@ -13,18 +13,22 @@ import { getRootSpan, spanToJSON, } from '@sentry/core'; -import { addOpenTelemetryInstrumentation } from '@sentry/opentelemetry'; import type { IntegrationFn, Span } from '@sentry/types'; import { logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../../../debug-build'; +import { generateInstrumentOnce } from '../../../otel/instrument'; import { ensureIsWrapped } from '../../../utils/ensureIsWrapped'; import type { Boom, RequestEvent, ResponseObject, Server } from './types'; +const INTEGRATION_NAME = 'Hapi'; + +export const instrumentHapi = generateInstrumentOnce(INTEGRATION_NAME, () => new HapiInstrumentation()); + const _hapiIntegration = (() => { return { - name: 'Hapi', + name: INTEGRATION_NAME, setupOnce() { - addOpenTelemetryInstrumentation(new HapiInstrumentation()); + instrumentHapi(); }, }; }) satisfies IntegrationFn; diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts index ec71ec7b8b60..55a01ba13651 100644 --- a/packages/node/src/integrations/tracing/index.ts +++ b/packages/node/src/integrations/tracing/index.ts @@ -1,17 +1,18 @@ import type { Integration } from '@sentry/types'; +import { instrumentHttp } from '../http'; -import { connectIntegration } from './connect'; -import { expressIntegration } from './express'; -import { fastifyIntegration } from './fastify'; -import { graphqlIntegration } from './graphql'; -import { hapiIntegration } from './hapi'; -import { koaIntegration } from './koa'; -import { mongoIntegration } from './mongo'; -import { mongooseIntegration } from './mongoose'; -import { mysqlIntegration } from './mysql'; -import { mysql2Integration } from './mysql2'; -import { nestIntegration } from './nest'; -import { postgresIntegration } from './postgres'; +import { connectIntegration, instrumentConnect } from './connect'; +import { expressIntegration, instrumentExpress } from './express'; +import { fastifyIntegration, instrumentFastify } from './fastify'; +import { graphqlIntegration, instrumentGraphql } from './graphql'; +import { hapiIntegration, instrumentHapi } from './hapi'; +import { instrumentKoa, koaIntegration } from './koa'; +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 { instrumentPostgres, postgresIntegration } from './postgres'; import { redisIntegration } from './redis'; /** @@ -38,3 +39,26 @@ export function getAutoPerformanceIntegrations(): Integration[] { connectIntegration(), ]; } + +/** + * Get a list of methods to instrument OTEL, when preload instrumentation. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function getOpenTelemetryInstrumentationToPreload(): (((options?: any) => void) & { id: string })[] { + return [ + instrumentHttp, + instrumentExpress, + instrumentConnect, + instrumentFastify, + instrumentHapi, + instrumentKoa, + instrumentNest, + instrumentMongo, + instrumentMongoose, + instrumentMysql, + instrumentMysql2, + instrumentPostgres, + instrumentHapi, + instrumentGraphql, + ]; +} diff --git a/packages/node/src/integrations/tracing/koa.ts b/packages/node/src/integrations/tracing/koa.ts index 7d68afb19efe..1fc85234fb76 100644 --- a/packages/node/src/integrations/tracing/koa.ts +++ b/packages/node/src/integrations/tracing/koa.ts @@ -9,56 +9,40 @@ import { getIsolationScope, spanToJSON, } from '@sentry/core'; -import { addOpenTelemetryInstrumentation } from '@sentry/opentelemetry'; import type { IntegrationFn, Span } from '@sentry/types'; import { logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../../debug-build'; +import { generateInstrumentOnce } from '../../otel/instrument'; import { ensureIsWrapped } from '../../utils/ensureIsWrapped'; -function addKoaSpanAttributes(span: Span): void { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.http.otel.koa'); - - const attributes = spanToJSON(span).data || {}; - - // this is one of: middleware, router - const type = attributes['koa.type']; +const INTEGRATION_NAME = 'Koa'; - if (type) { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, `${type}.koa`); - } +export const instrumentKoa = generateInstrumentOnce( + INTEGRATION_NAME, + () => + new KoaInstrumentation({ + requestHook(span, info) { + addKoaSpanAttributes(span); - // Also update the name - const name = attributes['koa.name']; - if (typeof name === 'string') { - // Somehow, name is sometimes `''` for middleware spans - // See: https://github.com/open-telemetry/opentelemetry-js-contrib/issues/2220 - span.updateName(name || '< unknown >'); - } -} + if (getIsolationScope() === getDefaultIsolationScope()) { + DEBUG_BUILD && logger.warn('Isolation scope is default isolation scope - skipping setting transactionName'); + return; + } + const attributes = spanToJSON(span).data; + const route = attributes && attributes[SEMATTRS_HTTP_ROUTE]; + const method = info.context.request.method.toUpperCase() || 'GET'; + if (route) { + getIsolationScope().setTransactionName(`${method} ${route}`); + } + }, + }), +); const _koaIntegration = (() => { return { - name: 'Koa', + name: INTEGRATION_NAME, setupOnce() { - addOpenTelemetryInstrumentation( - new KoaInstrumentation({ - requestHook(span, info) { - addKoaSpanAttributes(span); - - if (getIsolationScope() === getDefaultIsolationScope()) { - DEBUG_BUILD && - logger.warn('Isolation scope is default isolation scope - skipping setting transactionName'); - return; - } - const attributes = spanToJSON(span).data; - const route = attributes && attributes[SEMATTRS_HTTP_ROUTE]; - const method = info.context.request.method.toUpperCase() || 'GET'; - if (route) { - getIsolationScope().setTransactionName(`${method} ${route}`); - } - }, - }), - ); + instrumentKoa(); }, }; }) satisfies IntegrationFn; @@ -77,3 +61,24 @@ export const setupKoaErrorHandler = (app: { use: (arg0: (ctx: any, next: any) => ensureIsWrapped(app.use, 'koa'); }; + +function addKoaSpanAttributes(span: Span): void { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.http.otel.koa'); + + const attributes = spanToJSON(span).data || {}; + + // this is one of: middleware, router + const type = attributes['koa.type']; + + if (type) { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, `${type}.koa`); + } + + // Also update the name + const name = attributes['koa.name']; + if (typeof name === 'string') { + // Somehow, name is sometimes `''` for middleware spans + // See: https://github.com/open-telemetry/opentelemetry-js-contrib/issues/2220 + span.updateName(name || '< unknown >'); + } +} diff --git a/packages/node/src/integrations/tracing/mongo.ts b/packages/node/src/integrations/tracing/mongo.ts index 03442df058a6..143c7bf99a6d 100644 --- a/packages/node/src/integrations/tracing/mongo.ts +++ b/packages/node/src/integrations/tracing/mongo.ts @@ -1,21 +1,27 @@ import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'; import { defineIntegration } from '@sentry/core'; -import { addOpenTelemetryInstrumentation } from '@sentry/opentelemetry'; import type { IntegrationFn } from '@sentry/types'; +import { generateInstrumentOnce } from '../../otel/instrument'; import { addOriginToSpan } from '../../utils/addOriginToSpan'; +const INTEGRATION_NAME = 'Mongo'; + +export const instrumentMongo = generateInstrumentOnce( + INTEGRATION_NAME, + () => + new MongoDBInstrumentation({ + responseHook(span) { + addOriginToSpan(span, 'auto.db.otel.mongo'); + }, + }), +); + const _mongoIntegration = (() => { return { - name: 'Mongo', + name: INTEGRATION_NAME, setupOnce() { - addOpenTelemetryInstrumentation( - new MongoDBInstrumentation({ - responseHook(span) { - addOriginToSpan(span, 'auto.db.otel.mongo'); - }, - }), - ); + instrumentMongo(); }, }; }) satisfies IntegrationFn; diff --git a/packages/node/src/integrations/tracing/mongoose.ts b/packages/node/src/integrations/tracing/mongoose.ts index 13a11ca46937..4a4566fa98da 100644 --- a/packages/node/src/integrations/tracing/mongoose.ts +++ b/packages/node/src/integrations/tracing/mongoose.ts @@ -1,21 +1,27 @@ import { MongooseInstrumentation } from '@opentelemetry/instrumentation-mongoose'; import { defineIntegration } from '@sentry/core'; -import { addOpenTelemetryInstrumentation } from '@sentry/opentelemetry'; import type { IntegrationFn } from '@sentry/types'; +import { generateInstrumentOnce } from '../../otel/instrument'; import { addOriginToSpan } from '../../utils/addOriginToSpan'; +const INTEGRATION_NAME = 'Mongoose'; + +export const instrumentMongoose = generateInstrumentOnce( + INTEGRATION_NAME, + () => + new MongooseInstrumentation({ + responseHook(span) { + addOriginToSpan(span, 'auto.db.otel.mongoose'); + }, + }), +); + const _mongooseIntegration = (() => { return { - name: 'Mongoose', + name: INTEGRATION_NAME, setupOnce() { - addOpenTelemetryInstrumentation( - new MongooseInstrumentation({ - responseHook(span) { - addOriginToSpan(span, 'auto.db.otel.mongoose'); - }, - }), - ); + instrumentMongoose(); }, }; }) satisfies IntegrationFn; diff --git a/packages/node/src/integrations/tracing/mysql.ts b/packages/node/src/integrations/tracing/mysql.ts index 4ad0daca2a8b..67b46ae9bdcf 100644 --- a/packages/node/src/integrations/tracing/mysql.ts +++ b/packages/node/src/integrations/tracing/mysql.ts @@ -1,13 +1,17 @@ import { MySQLInstrumentation } from '@opentelemetry/instrumentation-mysql'; import { defineIntegration } from '@sentry/core'; -import { addOpenTelemetryInstrumentation } from '@sentry/opentelemetry'; import type { IntegrationFn } from '@sentry/types'; +import { generateInstrumentOnce } from '../../otel/instrument'; + +const INTEGRATION_NAME = 'Mysql'; + +export const instrumentMysql = generateInstrumentOnce(INTEGRATION_NAME, () => new MySQLInstrumentation({})); const _mysqlIntegration = (() => { return { - name: 'Mysql', + name: INTEGRATION_NAME, setupOnce() { - addOpenTelemetryInstrumentation(new MySQLInstrumentation({})); + instrumentMysql(); }, }; }) satisfies IntegrationFn; diff --git a/packages/node/src/integrations/tracing/mysql2.ts b/packages/node/src/integrations/tracing/mysql2.ts index 332560c1d5a1..b3c36435979c 100644 --- a/packages/node/src/integrations/tracing/mysql2.ts +++ b/packages/node/src/integrations/tracing/mysql2.ts @@ -1,21 +1,27 @@ import { MySQL2Instrumentation } from '@opentelemetry/instrumentation-mysql2'; import { defineIntegration } from '@sentry/core'; -import { addOpenTelemetryInstrumentation } from '@sentry/opentelemetry'; import type { IntegrationFn } from '@sentry/types'; +import { generateInstrumentOnce } from '../../otel/instrument'; import { addOriginToSpan } from '../../utils/addOriginToSpan'; +const INTEGRATION_NAME = 'Mysql2'; + +export const instrumentMysql2 = generateInstrumentOnce( + INTEGRATION_NAME, + () => + new MySQL2Instrumentation({ + responseHook(span) { + addOriginToSpan(span, 'auto.db.otel.mysql2'); + }, + }), +); + const _mysql2Integration = (() => { return { - name: 'Mysql2', + name: INTEGRATION_NAME, setupOnce() { - addOpenTelemetryInstrumentation( - new MySQL2Instrumentation({ - responseHook(span) { - addOriginToSpan(span, 'auto.db.otel.mysql2'); - }, - }), - ); + instrumentMysql2(); }, }; }) satisfies IntegrationFn; diff --git a/packages/node/src/integrations/tracing/nest.ts b/packages/node/src/integrations/tracing/nest.ts index cc66e745da1d..bbb658318946 100644 --- a/packages/node/src/integrations/tracing/nest.ts +++ b/packages/node/src/integrations/tracing/nest.ts @@ -9,9 +9,9 @@ import { getIsolationScope, spanToJSON, } from '@sentry/core'; -import { addOpenTelemetryInstrumentation } from '@sentry/opentelemetry'; import type { IntegrationFn, Span } from '@sentry/types'; import { logger } from '@sentry/utils'; +import { generateInstrumentOnce } from '../../otel/instrument'; interface MinimalNestJsExecutionContext { getType: () => string; @@ -37,15 +37,20 @@ interface NestJsErrorFilter { 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'; + +export const instrumentNest = generateInstrumentOnce(INTEGRATION_NAME, () => new NestInstrumentation()); + const _nestIntegration = (() => { return { - name: 'Nest', + name: INTEGRATION_NAME, setupOnce() { - addOpenTelemetryInstrumentation(new NestInstrumentation({})); + instrumentNest(); }, }; }) satisfies IntegrationFn; diff --git a/packages/node/src/integrations/tracing/postgres.ts b/packages/node/src/integrations/tracing/postgres.ts index ad662d123845..05b56d9152ff 100644 --- a/packages/node/src/integrations/tracing/postgres.ts +++ b/packages/node/src/integrations/tracing/postgres.ts @@ -1,22 +1,28 @@ import { PgInstrumentation } from '@opentelemetry/instrumentation-pg'; import { defineIntegration } from '@sentry/core'; -import { addOpenTelemetryInstrumentation } from '@sentry/opentelemetry'; import type { IntegrationFn } from '@sentry/types'; +import { generateInstrumentOnce } from '../../otel/instrument'; import { addOriginToSpan } from '../../utils/addOriginToSpan'; +const INTEGRATION_NAME = 'Postgres'; + +export const instrumentPostgres = generateInstrumentOnce( + INTEGRATION_NAME, + () => + new PgInstrumentation({ + requireParentSpan: true, + requestHook(span) { + addOriginToSpan(span, 'auto.db.otel.postgres'); + }, + }), +); + const _postgresIntegration = (() => { return { - name: 'Postgres', + name: INTEGRATION_NAME, setupOnce() { - addOpenTelemetryInstrumentation( - new PgInstrumentation({ - requireParentSpan: true, - requestHook(span) { - addOriginToSpan(span, 'auto.db.otel.postgres'); - }, - }), - ); + instrumentPostgres(); }, }; }) satisfies IntegrationFn; diff --git a/packages/node/src/integrations/tracing/prisma.ts b/packages/node/src/integrations/tracing/prisma.ts index 7652ea793530..e5d9e61a0229 100644 --- a/packages/node/src/integrations/tracing/prisma.ts +++ b/packages/node/src/integrations/tracing/prisma.ts @@ -1,17 +1,25 @@ // When importing CJS modules into an ESM module, we cannot import the named exports directly. import * as prismaInstrumentation from '@prisma/instrumentation'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, defineIntegration, spanToJSON } from '@sentry/core'; -import { addOpenTelemetryInstrumentation } from '@sentry/opentelemetry'; import type { IntegrationFn } from '@sentry/types'; +import { generateInstrumentOnce } from '../../otel/instrument'; + +const INTEGRATION_NAME = 'Prisma'; + +export const instrumentPrisma = generateInstrumentOnce(INTEGRATION_NAME, () => { + const EsmInteropPrismaInstrumentation: typeof prismaInstrumentation.PrismaInstrumentation = + // @ts-expect-error We need to do the following for interop reasons + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + prismaInstrumentation.default?.PrismaInstrumentation || prismaInstrumentation.PrismaInstrumentation; + + return new EsmInteropPrismaInstrumentation({}); +}); const _prismaIntegration = (() => { return { - name: 'Prisma', + name: INTEGRATION_NAME, setupOnce() { - addOpenTelemetryInstrumentation( - // does not have a hook to adjust spans & add origin - new prismaInstrumentation.PrismaInstrumentation({}), - ); + instrumentPrisma(); }, setup(client) { diff --git a/packages/node/src/integrations/tracing/redis.ts b/packages/node/src/integrations/tracing/redis.ts index f9309b2de33e..1379336412f6 100644 --- a/packages/node/src/integrations/tracing/redis.ts +++ b/packages/node/src/integrations/tracing/redis.ts @@ -4,11 +4,12 @@ import { SEMANTIC_ATTRIBUTE_CACHE_ITEM_SIZE, SEMANTIC_ATTRIBUTE_CACHE_KEY, SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, defineIntegration, spanToJSON, } from '@sentry/core'; -import { addOpenTelemetryInstrumentation } from '@sentry/opentelemetry'; import type { IntegrationFn } from '@sentry/types'; +import { generateInstrumentOnce } from '../../otel/instrument'; function keyHasPrefix(key: string, prefixes: string[]): boolean { return prefixes.some(prefix => key.startsWith(prefix)); @@ -40,54 +41,64 @@ interface RedisOptions { cachePrefixes?: string[]; } -const _redisIntegration = ((options?: RedisOptions) => { - return { - name: 'Redis', - setupOnce() { - addOpenTelemetryInstrumentation([ - new IORedisInstrumentation({ - responseHook: (span, redisCommand, cmdArgs, response) => { - const key = cmdArgs[0]; +const INTEGRATION_NAME = 'Redis'; + +let _redisOptions: RedisOptions = {}; + +export const instrumentRedis = generateInstrumentOnce(INTEGRATION_NAME, () => { + return new IORedisInstrumentation({ + responseHook: (span, redisCommand, cmdArgs, response) => { + const key = cmdArgs[0]; + + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.redis'); - if (!options?.cachePrefixes || !shouldConsiderForCache(redisCommand, key, options.cachePrefixes)) { - // not relevant for cache - return; - } + if (!_redisOptions?.cachePrefixes || !shouldConsiderForCache(redisCommand, key, _redisOptions.cachePrefixes)) { + // not relevant for cache + return; + } - // otel/ioredis seems to be using the old standard, as there was a change to those params: https://github.com/open-telemetry/opentelemetry-specification/issues/3199 - // We are using params based on the docs: https://opentelemetry.io/docs/specs/semconv/attributes-registry/network/ - const networkPeerAddress = spanToJSON(span).data?.['net.peer.name']; - const networkPeerPort = spanToJSON(span).data?.['net.peer.port']; - if (networkPeerPort && networkPeerAddress) { - span.setAttributes({ 'network.peer.address': networkPeerAddress, 'network.peer.port': networkPeerPort }); - } + // otel/ioredis seems to be using the old standard, as there was a change to those params: https://github.com/open-telemetry/opentelemetry-specification/issues/3199 + // We are using params based on the docs: https://opentelemetry.io/docs/specs/semconv/attributes-registry/network/ + const networkPeerAddress = spanToJSON(span).data?.['net.peer.name']; + const networkPeerPort = spanToJSON(span).data?.['net.peer.port']; + if (networkPeerPort && networkPeerAddress) { + span.setAttributes({ 'network.peer.address': networkPeerAddress, 'network.peer.port': networkPeerPort }); + } - const cacheItemSize = calculateCacheItemSize(response); - if (cacheItemSize) span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_ITEM_SIZE, cacheItemSize); + const cacheItemSize = calculateCacheItemSize(response); + if (cacheItemSize) span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_ITEM_SIZE, cacheItemSize); + + if (typeof key === 'string') { + switch (redisCommand) { + case 'get': + span.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get_item', // todo: will be changed to cache.get + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: key, + }); + if (cacheItemSize !== undefined) span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_HIT, cacheItemSize > 0); + break; + case 'set': + span.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.put', + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: key, + }); + break; + } + } + }, + }); +}); + +const _redisIntegration = ((options: RedisOptions = {}) => { + return { + name: INTEGRATION_NAME, + setupOnce() { + _redisOptions = options; + instrumentRedis(); - if (typeof key === 'string') { - switch (redisCommand) { - case 'get': - span.setAttributes({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get_item', // todo: will be changed to cache.get - [SEMANTIC_ATTRIBUTE_CACHE_KEY]: key, - }); - if (cacheItemSize !== undefined) span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_HIT, cacheItemSize > 0); - break; - case 'set': - span.setAttributes({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.put', - [SEMANTIC_ATTRIBUTE_CACHE_KEY]: key, - }); - break; - } - } - }, - }), - // todo: implement them gradually - // new LegacyRedisInstrumentation({}), - // new RedisInstrumentation({}), - ]); + // todo: implement them gradually + // new LegacyRedisInstrumentation({}), + // new RedisInstrumentation({}), }, }; }) satisfies IntegrationFn; diff --git a/packages/node/src/otel/instrument.ts b/packages/node/src/otel/instrument.ts new file mode 100644 index 000000000000..71cc28a24915 --- /dev/null +++ b/packages/node/src/otel/instrument.ts @@ -0,0 +1,31 @@ +import type { Instrumentation } from '@opentelemetry/instrumentation'; +import { addOpenTelemetryInstrumentation } from '@sentry/opentelemetry'; + +const INSTRUMENTED: Record = {}; + +/** + * Instrument an OpenTelemetry instrumentation once. + * This will skip running instrumentation again if it was already instrumented. + */ +export function generateInstrumentOnce( + name: string, + creator: (options?: Options) => Instrumentation, +): ((options?: Options) => void) & { id: string } { + return Object.assign( + (options?: Options) => { + if (INSTRUMENTED[name]) { + // If options are provided, ensure we update them + if (options) { + INSTRUMENTED[name].setConfig(options); + } + return; + } + + const instrumentation = creator(options); + INSTRUMENTED[name] = instrumentation; + + addOpenTelemetryInstrumentation(instrumentation); + }, + { id: name }, + ); +} diff --git a/packages/node/src/preload.ts b/packages/node/src/preload.ts new file mode 100644 index 000000000000..0d62b28d9c91 --- /dev/null +++ b/packages/node/src/preload.ts @@ -0,0 +1,19 @@ +import { preloadOpenTelemetry } from './sdk/initOtel'; + +const debug = !!process.env.SENTRY_DEBUG; +const integrationsStr = process.env.SENTRY_PRELOAD_INTEGRATIONS; + +const integrations = integrationsStr ? integrationsStr.split(',').map(integration => integration.trim()) : undefined; + +/** + * The @sentry/node/preload export can be used with the node --import and --require args to preload the OTEL instrumentation, + * without initializing the Sentry SDK. + * + * This is useful if you cannot initialize the SDK immediately, but still want to preload the instrumentation, + * e.g. if you have to load the DSN from somewhere else. + * + * You can configure this in two ways via environment variables: + * - `SENTRY_DEBUG` to enable debug logging + * - `SENTRY_PRELOAD_INTEGRATIONS` to preload specific integrations - e.g. `SENTRY_PRELOAD_INTEGRATIONS="Http,Express"` + */ +preloadOpenTelemetry({ debug, integrations }); diff --git a/packages/node/src/sdk/init.ts b/packages/node/src/sdk/index.ts similarity index 85% rename from packages/node/src/sdk/init.ts rename to packages/node/src/sdk/index.ts index 82cd26492960..f149a44c06a0 100644 --- a/packages/node/src/sdk/init.ts +++ b/packages/node/src/sdk/index.ts @@ -11,10 +11,13 @@ import { requestDataIntegration, startSession, } from '@sentry/core'; -import { openTelemetrySetupCheck, setOpenTelemetryContextAsyncContextStrategy } from '@sentry/opentelemetry'; +import { + openTelemetrySetupCheck, + setOpenTelemetryContextAsyncContextStrategy, + setupEventContextTrace, +} from '@sentry/opentelemetry'; import type { Client, Integration, Options } from '@sentry/types'; import { - GLOBAL_OBJ, consoleSandbox, dropUndefinedKeys, logger, @@ -26,7 +29,6 @@ import { consoleIntegration } from '../integrations/console'; import { nodeContextIntegration } from '../integrations/context'; import { contextLinesIntegration } from '../integrations/contextlines'; -import moduleModule from 'module'; import { httpIntegration } from '../integrations/http'; import { localVariablesIntegration } from '../integrations/local-variables'; import { modulesIntegration } from '../integrations/modules'; @@ -40,7 +42,7 @@ import type { NodeClientOptions, NodeOptions } from '../types'; import { isCjs } from '../utils/commonjs'; import { defaultStackParser, getSentryRelease } from './api'; import { NodeClient } from './client'; -import { initOpenTelemetry } from './initOtel'; +import { initOpenTelemetry, maybeInitializeEsmLoader } from './initOtel'; function getCjsOnlyIntegrations(): Integration[] { return isCjs() ? [modulesIntegration()] : []; @@ -92,8 +94,6 @@ function shouldAddPerformanceIntegrations(options: Options): boolean { return options.enableTracing || options.tracesSampleRate != null || 'tracesSampler' in options; } -declare const __IMPORT_META_URL_REPLACEMENT__: string; - /** * Initialize Sentry for Node. */ @@ -130,31 +130,7 @@ function _init( } if (!isCjs()) { - const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(Number); - - // Register hook was added in v20.6.0 and v18.19.0 - if (nodeMajor >= 22 || (nodeMajor === 20 && nodeMinor >= 6) || (nodeMajor === 18 && nodeMinor >= 19)) { - // We need to work around using import.meta.url directly because jest complains about it. - const importMetaUrl = - typeof __IMPORT_META_URL_REPLACEMENT__ !== 'undefined' ? __IMPORT_META_URL_REPLACEMENT__ : undefined; - - if (!GLOBAL_OBJ._sentryEsmLoaderHookRegistered && importMetaUrl) { - try { - // @ts-expect-error register is available in these versions - moduleModule.register('@opentelemetry/instrumentation/hook.mjs', importMetaUrl); - GLOBAL_OBJ._sentryEsmLoaderHookRegistered = true; - } catch (error) { - logger.warn('Failed to register ESM hook', error); - } - } - } else { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn( - '[Sentry] You are using Node.js in ESM mode ("import syntax"). The Sentry Node.js SDK is not compatible with ESM in Node.js versions before 18.19.0 or before 20.6.0. Please either build your application with CommonJS ("require() syntax"), or use version 7.x of the Sentry Node.js SDK.', - ); - }); - } + maybeInitializeEsmLoader(); } setOpenTelemetryContextAsyncContextStrategy(); @@ -196,12 +172,16 @@ function _init( // There is no way to use this SDK without OpenTelemetry! if (!options.skipOpenTelemetrySetup) { initOpenTelemetry(client); + validateOpenTelemetrySetup(); } - validateOpenTelemetrySetup(); + setupEventContextTrace(client); } -function validateOpenTelemetrySetup(): void { +/** + * Validate that your OpenTelemetry setup is correct. + */ +export function validateOpenTelemetrySetup(): void { if (!DEBUG_BUILD) { return; } diff --git a/packages/node/src/sdk/initOtel.ts b/packages/node/src/sdk/initOtel.ts index f27635610c9c..26a2f34e0901 100644 --- a/packages/node/src/sdk/initOtel.ts +++ b/packages/node/src/sdk/initOtel.ts @@ -1,3 +1,4 @@ +import moduleModule from 'module'; import { DiagLogLevel, diag } from '@opentelemetry/api'; import { Resource } from '@opentelemetry/resources'; import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; @@ -7,31 +8,97 @@ import { SEMRESATTRS_SERVICE_VERSION, } from '@opentelemetry/semantic-conventions'; import { SDK_VERSION } from '@sentry/core'; -import { SentryPropagator, SentrySampler, SentrySpanProcessor, setupEventContextTrace } from '@sentry/opentelemetry'; -import { logger } from '@sentry/utils'; +import { SentryPropagator, SentrySampler, SentrySpanProcessor } from '@sentry/opentelemetry'; +import { GLOBAL_OBJ, consoleSandbox, logger } from '@sentry/utils'; +import { getOpenTelemetryInstrumentationToPreload } from '../integrations/tracing'; import { SentryContextManager } from '../otel/contextManager'; +import { isCjs } from '../utils/commonjs'; import type { NodeClient } from './client'; +declare const __IMPORT_META_URL_REPLACEMENT__: string; + /** * Initialize OpenTelemetry for Node. */ export function initOpenTelemetry(client: NodeClient): void { if (client.getOptions().debug) { - const otelLogger = new Proxy(logger as typeof logger & { verbose: (typeof logger)['debug'] }, { - get(target, prop, receiver) { - const actualProp = prop === 'verbose' ? 'debug' : prop; - return Reflect.get(target, actualProp, receiver); - }, + setupOpenTelemetryLogger(); + } + + const provider = setupOtel(client); + client.traceProvider = provider; +} + +/** Initialize the ESM loader. */ +export function maybeInitializeEsmLoader(): void { + const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(Number); + + // Register hook was added in v20.6.0 and v18.19.0 + if (nodeMajor >= 22 || (nodeMajor === 20 && nodeMinor >= 6) || (nodeMajor === 18 && nodeMinor >= 19)) { + // We need to work around using import.meta.url directly because jest complains about it. + const importMetaUrl = + typeof __IMPORT_META_URL_REPLACEMENT__ !== 'undefined' ? __IMPORT_META_URL_REPLACEMENT__ : undefined; + + if (!GLOBAL_OBJ._sentryEsmLoaderHookRegistered && importMetaUrl) { + try { + // @ts-expect-error register is available in these versions + moduleModule.register('@opentelemetry/instrumentation/hook.mjs', importMetaUrl); + GLOBAL_OBJ._sentryEsmLoaderHookRegistered = true; + } catch (error) { + logger.warn('Failed to register ESM hook', error); + } + } + } else { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + '[Sentry] You are using Node.js in ESM mode ("import syntax"). The Sentry Node.js SDK is not compatible with ESM in Node.js versions before 18.19.0 or before 20.6.0. Please either build your application with CommonJS ("require() syntax"), or use version 7.x of the Sentry Node.js SDK.', + ); }); + } +} + +interface NodePreloadOptions { + debug?: boolean; + integrations?: string[]; +} + +/** + * Preload OpenTelemetry for Node. + * This can be used to preload instrumentation early, but set up Sentry later. + * By preloading the OTEL instrumentation wrapping still happens early enough that everything works. + */ +export function preloadOpenTelemetry(options: NodePreloadOptions = {}): void { + const { debug } = options; - diag.setLogger(otelLogger, DiagLogLevel.DEBUG); + if (debug) { + logger.enable(); + setupOpenTelemetryLogger(); } - setupEventContextTrace(client); + if (!isCjs()) { + maybeInitializeEsmLoader(); + } - const provider = setupOtel(client); - client.traceProvider = provider; + // These are all integrations that we need to pre-load to ensure they are set up before any other code runs + getPreloadMethods(options.integrations).forEach(fn => { + fn(); + + if (debug) { + logger.log(`[Sentry] Preloaded ${fn.id} instrumentation`); + } + }); +} + +function getPreloadMethods(integrationNames?: string[]): ((() => void) & { id: string })[] { + const instruments = getOpenTelemetryInstrumentationToPreload(); + + if (!integrationNames) { + return instruments; + } + + return instruments.filter(instrumentation => integrationNames.includes(instrumentation.id)); } /** Just exported for tests. */ @@ -56,3 +123,19 @@ export function setupOtel(client: NodeClient): BasicTracerProvider { return provider; } + +/** + * Setup the OTEL logger to use our own logger. + */ +function setupOpenTelemetryLogger(): void { + const otelLogger = new Proxy(logger as typeof logger & { verbose: (typeof logger)['debug'] }, { + get(target, prop, receiver) { + const actualProp = prop === 'verbose' ? 'debug' : prop; + return Reflect.get(target, actualProp, receiver); + }, + }); + + // Disable diag, to ensure this works even if called multiple times + diag.disable(); + diag.setLogger(otelLogger, DiagLogLevel.DEBUG); +} diff --git a/packages/node/test/helpers/mockSdkInit.ts b/packages/node/test/helpers/mockSdkInit.ts index 845721868d76..0e1d23cfc73c 100644 --- a/packages/node/test/helpers/mockSdkInit.ts +++ b/packages/node/test/helpers/mockSdkInit.ts @@ -3,7 +3,7 @@ import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import { getClient, getCurrentScope, getGlobalScope, getIsolationScope } from '@sentry/core'; import type { NodeClient } from '../../src'; -import { init } from '../../src/sdk/init'; +import { init } from '../../src/sdk'; import type { NodeClientOptions } from '../../src/types'; const PUBLIC_DSN = 'https://username@domain/123'; diff --git a/packages/node/test/sdk/init.test.ts b/packages/node/test/sdk/init.test.ts index d1c3788caa2f..5592acfaa897 100644 --- a/packages/node/test/sdk/init.test.ts +++ b/packages/node/test/sdk/init.test.ts @@ -2,8 +2,8 @@ import type { Integration } from '@sentry/types'; import { getClient } from '../../src/'; import * as auto from '../../src/integrations/tracing'; +import { init } from '../../src/sdk'; import type { NodeClient } from '../../src/sdk/client'; -import { init } from '../../src/sdk/init'; import { cleanupOtel } from '../helpers/mockSdkInit'; // eslint-disable-next-line no-var diff --git a/packages/node/test/sdk/preload.test.ts b/packages/node/test/sdk/preload.test.ts new file mode 100644 index 000000000000..fedba139b0f6 --- /dev/null +++ b/packages/node/test/sdk/preload.test.ts @@ -0,0 +1,50 @@ +import { logger } from '@sentry/utils'; + +describe('preload', () => { + afterEach(() => { + jest.resetAllMocks(); + logger.disable(); + + delete process.env.SENTRY_DEBUG; + delete process.env.SENTRY_PRELOAD_INTEGRATIONS; + + jest.resetModules(); + }); + + it('works without env vars', async () => { + const logSpy = jest.spyOn(console, 'log'); + + await import('../../src/preload'); + + expect(logSpy).toHaveBeenCalledTimes(0); + }); + + it('works with SENTRY_DEBUG set', async () => { + const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + // We want to swallow these logs + jest.spyOn(console, 'debug').mockImplementation(() => {}); + + process.env.SENTRY_DEBUG = '1'; + + await import('../../src/preload'); + + expect(logSpy).toHaveBeenCalledWith('Sentry Logger [log]:', '[Sentry] Preloaded Http instrumentation'); + expect(logSpy).toHaveBeenCalledWith('Sentry Logger [log]:', '[Sentry] Preloaded Express instrumentation'); + expect(logSpy).toHaveBeenCalledWith('Sentry Logger [log]:', '[Sentry] Preloaded Graphql instrumentation'); + }); + + it('works with SENTRY_DEBUG & SENTRY_PRELOAD_INTEGRATIONS set', async () => { + const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + // We want to swallow these logs + jest.spyOn(console, 'debug').mockImplementation(() => {}); + + process.env.SENTRY_DEBUG = '1'; + process.env.SENTRY_PRELOAD_INTEGRATIONS = 'Http,Express'; + + await import('../../src/preload'); + + expect(logSpy).toHaveBeenCalledWith('Sentry Logger [log]:', '[Sentry] Preloaded Http instrumentation'); + expect(logSpy).toHaveBeenCalledWith('Sentry Logger [log]:', '[Sentry] Preloaded Express instrumentation'); + expect(logSpy).not.toHaveBeenCalledWith('Sentry Logger [log]:', '[Sentry] Preloaded Graphql instrumentation'); + }); +}); diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts index ce08f0310319..f8a9ae4e5e4d 100644 --- a/packages/profiling-node/src/integration.ts +++ b/packages/profiling-node/src/integration.ts @@ -5,6 +5,7 @@ import type { IntegrationFn, Span } from '@sentry/types'; import { logger } from '@sentry/utils'; import { DEBUG_BUILD } from './debug-build'; +import { NODE_MAJOR, NODE_VERSION } from './nodeVersion'; import { MAX_PROFILE_DURATION_MS, maybeProfileSpan, stopSpanProfile } from './spanProfileUtils'; import type { Profile, RawThreadCpuProfile } from './types'; @@ -25,6 +26,15 @@ function addToProfileQueue(profile: RawThreadCpuProfile): void { /** Exported only for tests. */ export const _nodeProfilingIntegration = (() => { + if (DEBUG_BUILD && ![16, 18, 20, 22].includes(NODE_MAJOR)) { + logger.warn( + `[Profiling] You are using a Node.js version that does not have prebuilt binaries (${NODE_VERSION}).`, + 'The @sentry/profiling-node package only has prebuilt support for the following LTS versions of Node.js: 16, 18, 20, 22.', + 'To use the @sentry/profiling-node package with this version of Node.js, you will need to compile the native addon from source.', + 'See: https://github.com/getsentry/sentry-javascript/tree/develop/packages/profiling-node#building-the-package-from-source', + ); + } + return { name: 'ProfilingIntegration', setup(client: NodeClient) { diff --git a/packages/profiling-node/src/nodeVersion.ts b/packages/profiling-node/src/nodeVersion.ts new file mode 100644 index 000000000000..1f07883b771b --- /dev/null +++ b/packages/profiling-node/src/nodeVersion.ts @@ -0,0 +1,4 @@ +import { parseSemver } from '@sentry/utils'; + +export const NODE_VERSION = parseSemver(process.versions.node) as { major: number; minor: number; patch: number }; +export const NODE_MAJOR = NODE_VERSION.major; diff --git a/packages/react/package.json b/packages/react/package.json index 50e15e27ae5c..ee3c5b669aae 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -49,7 +49,7 @@ "hoist-non-react-statics": "^3.3.2" }, "peerDependencies": { - "react": "16.x || 17.x || 18.x" + "react": "^16.14.0 || 17.x || 18.x || 19.x" }, "devDependencies": { "@testing-library/react": "^13.0.0", diff --git a/packages/react/rollup.npm.config.mjs b/packages/react/rollup.npm.config.mjs index d87739380bd6..e4b28f60a4a6 100644 --- a/packages/react/rollup.npm.config.mjs +++ b/packages/react/rollup.npm.config.mjs @@ -3,5 +3,12 @@ import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollu export default makeNPMConfigVariants( makeBaseNPMConfig({ esModuleInterop: true, + packageSpecificConfig: { + external: ['react', 'react/jsx-runtime'], + }, + sucrase: { + jsxRuntime: 'automatic', // React 19 emits a warning if we don't use the newer jsx transform: https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html + production: true, // This is needed so that sucrase uses the production jsx runtime (ie `import { jsx } from 'react/jsx-runtime'` instead of `import { jsxDEV as _jsxDEV } from 'react/jsx-dev-runtime'`) + }, }), ); diff --git a/packages/remix/rollup.npm.config.mjs b/packages/remix/rollup.npm.config.mjs index b705fba0c55f..60779af94f6b 100644 --- a/packages/remix/rollup.npm.config.mjs +++ b/packages/remix/rollup.npm.config.mjs @@ -5,12 +5,16 @@ export default [ makeBaseNPMConfig({ entrypoints: ['src/index.server.ts', 'src/index.client.tsx'], packageSpecificConfig: { - external: ['react-router', 'react-router-dom'], + external: ['react-router', 'react-router-dom', 'react', 'react/jsx-runtime'], output: { // make it so Rollup calms down about the fact that we're combining default and named exports exports: 'named', }, }, + sucrase: { + jsxRuntime: 'automatic', // React 19 emits a warning if we don't use the newer jsx transform: https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html + production: true, // This is needed so that sucrase uses the production jsx runtime (ie `import { jsx } from 'react/jsx-runtime'` instead of `import { jsxDEV as _jsxDEV } from 'react/jsx-dev-runtime'`) + }, }), ), ...makeOtelLoaders('./build', 'sentry-node'), diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index ab35b4bf4847..a6476b692fbf 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -67,6 +67,7 @@ export { startSpan, startSpanManual, startInactiveSpan, + startNewTrace, withActiveSpan, getSpanDescendants, continueTrace, diff --git a/packages/replay-internal/src/coreHandlers/handleBeforeSendEvent.ts b/packages/replay-internal/src/coreHandlers/handleBeforeSendEvent.ts index 9427bc60e45b..297d0bc2bbe3 100644 --- a/packages/replay-internal/src/coreHandlers/handleBeforeSendEvent.ts +++ b/packages/replay-internal/src/coreHandlers/handleBeforeSendEvent.ts @@ -29,7 +29,10 @@ function handleHydrationError(replay: ReplayContainer, event: ErrorEvent): void if ( // Only matches errors in production builds of react-dom // Example https://reactjs.org/docs/error-decoder.html?invariant=423 - exceptionValue.match(/reactjs\.org\/docs\/error-decoder\.html\?invariant=(418|419|422|423|425)/) || + // With newer React versions, the messages changed to a different website https://react.dev/errors/418 + exceptionValue.match( + /(reactjs\.org\/docs\/error-decoder\.html\?invariant=|react\.dev\/errors\/)(418|419|422|423|425)/, + ) || // Development builds of react-dom // Error 1: Hydration failed because the initial UI does not match what was rendered on the server. // Error 2: Text content does not match server-rendered HTML. Warning: Text content did not match. diff --git a/packages/solidjs/.eslintrc.js b/packages/solidjs/.eslintrc.js new file mode 100644 index 000000000000..46d8d10cc538 --- /dev/null +++ b/packages/solidjs/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + env: { + browser: true, + }, + extends: ['../../.eslintrc.js'], +}; diff --git a/packages/solidjs/LICENSE b/packages/solidjs/LICENSE new file mode 100644 index 000000000000..63e7eb28e19c --- /dev/null +++ b/packages/solidjs/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Functional Software, Inc. dba Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/solidjs/README.md b/packages/solidjs/README.md new file mode 100644 index 000000000000..3e37b30e7032 --- /dev/null +++ b/packages/solidjs/README.md @@ -0,0 +1,9 @@ +

+ + Sentry + +

+ +# Official Sentry SDK for SolidJS + +This SDK is work in progress, and should not be used before officially released. diff --git a/packages/solidjs/package.json b/packages/solidjs/package.json new file mode 100644 index 000000000000..1db62ee002c4 --- /dev/null +++ b/packages/solidjs/package.json @@ -0,0 +1,81 @@ +{ + "name": "@sentry/solidjs", + "version": "8.4.0", + "description": "Official Sentry SDK for SolidJS", + "repository": "git://github.com/getsentry/sentry-javascript.git", + "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/solidjs", + "author": "Sentry", + "license": "MIT", + "engines": { + "node": ">=14.18" + }, + "files": [ + "cjs", + "esm", + "types", + "types-ts3.8" + ], + "main": "build/cjs/index.js", + "module": "build/esm/index.js", + "types": "build/types/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./build/types/index.d.ts", + "default": "./build/esm/index.js" + }, + "require": { + "types": "./build/types/index.d.ts", + "default": "./build/cjs/index.js" + } + } + }, + "typesVersions": { + "<4.9": { + "build/types/index.d.ts": [ + "build/types-ts3.8/index.d.ts" + ] + } + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@sentry/browser": "8.4.0", + "@sentry/core": "8.4.0", + "@sentry/types": "8.4.0" + }, + "peerDependencies": { + "solid-js": "1.8.x" + }, + "devDependencies": { + "@solidjs/testing-library": "0.8.5", + "vite-plugin-solid": "^2.8.2", + "solid-js": "1.8.11" + }, + "scripts": { + "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:downlevel", + "build:types:core": "tsc -p tsconfig.types.json", + "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", + "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": "ts-node ../../scripts/prepack.ts && npm pack ./build", + "circularDepCheck": "madge --circular src/index.ts", + "clean": "rimraf build coverage sentry-solidjs-*.tgz", + "fix": "eslint . --format stylish --fix", + "lint": "eslint . --format stylish", + "test": "vitest run", + "test:watch": "vitest --watch", + "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" + }, + "volta": { + "extends": "../../package.json" + }, + "sideEffects": false +} diff --git a/packages/solidjs/rollup.npm.config.mjs b/packages/solidjs/rollup.npm.config.mjs new file mode 100644 index 000000000000..84a06f2fb64a --- /dev/null +++ b/packages/solidjs/rollup.npm.config.mjs @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/solidjs/src/index.ts b/packages/solidjs/src/index.ts new file mode 100644 index 000000000000..8e25b84c4a0c --- /dev/null +++ b/packages/solidjs/src/index.ts @@ -0,0 +1,3 @@ +export * from '@sentry/browser'; + +export { init } from './sdk'; diff --git a/packages/solidjs/src/sdk.ts b/packages/solidjs/src/sdk.ts new file mode 100644 index 000000000000..7e33431a63b1 --- /dev/null +++ b/packages/solidjs/src/sdk.ts @@ -0,0 +1,16 @@ +import type { BrowserOptions } from '@sentry/browser'; +import { init as browserInit } from '@sentry/browser'; +import { applySdkMetadata } from '@sentry/core'; + +/** + * Initializes the SolidJS SDK + */ +export function init(options: BrowserOptions): void { + const opts = { + ...options, + }; + + applySdkMetadata(opts, 'solidjs'); + + browserInit(opts); +} diff --git a/packages/solidjs/test/sdk.test.ts b/packages/solidjs/test/sdk.test.ts new file mode 100644 index 000000000000..6b075b0099c4 --- /dev/null +++ b/packages/solidjs/test/sdk.test.ts @@ -0,0 +1,32 @@ +import { SDK_VERSION } from '@sentry/browser'; +import * as SentryBrowser from '@sentry/browser'; + +import { vi } from 'vitest'; +import { init as solidInit } from '../src/sdk'; + +const browserInit = vi.spyOn(SentryBrowser, 'init'); + +describe('Initialize SolidJS SDk', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('has the correct metadata', () => { + solidInit({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + }); + + const expectedMetadata = { + _metadata: { + sdk: { + name: 'sentry.javascript.solidjs', + packages: [{ name: 'npm:@sentry/solidjs', version: SDK_VERSION }], + version: SDK_VERSION, + }, + }, + }; + + expect(browserInit).toHaveBeenCalledTimes(1); + expect(browserInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata)); + }); +}); diff --git a/packages/solidjs/tsconfig.json b/packages/solidjs/tsconfig.json new file mode 100644 index 000000000000..b0eb9ecb6476 --- /dev/null +++ b/packages/solidjs/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + + "include": ["src/**/*"], + + "compilerOptions": {} +} diff --git a/packages/solidjs/tsconfig.test.json b/packages/solidjs/tsconfig.test.json new file mode 100644 index 000000000000..fc9e549d35ce --- /dev/null +++ b/packages/solidjs/tsconfig.test.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + + "include": ["test/**/*", "vite.config.ts"], + + "compilerOptions": { + // should include all types from `./tsconfig.json` plus types for all test frameworks used + "types": ["vitest/globals"] + + // other package-specific, test-specific options + } +} diff --git a/packages/solidjs/tsconfig.types.json b/packages/solidjs/tsconfig.types.json new file mode 100644 index 000000000000..65455f66bd75 --- /dev/null +++ b/packages/solidjs/tsconfig.types.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "build/types" + } +} diff --git a/packages/solidjs/vite.config.ts b/packages/solidjs/vite.config.ts new file mode 100644 index 000000000000..1dfe27d70c66 --- /dev/null +++ b/packages/solidjs/vite.config.ts @@ -0,0 +1,14 @@ +import solidPlugin from 'vite-plugin-solid'; +import type { UserConfig } from 'vitest'; +import baseConfig from '../../vite/vite.config'; + +export default { + ...baseConfig, + plugins: [solidPlugin({ hot: !process.env.VITEST })], + test: { + // test exists, no idea why TS doesn't recognize it + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...(baseConfig as UserConfig & { test: any }).test, + environment: 'jsdom', + }, +}; diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index d7d50f64481e..c8b97029e456 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -60,6 +60,7 @@ export { startSpan, startInactiveSpan, startSpanManual, + startNewTrace, withActiveSpan, continueTrace, cron, diff --git a/packages/types/src/context.ts b/packages/types/src/context.ts index 40d3822d6b3c..0344dd179787 100644 --- a/packages/types/src/context.ts +++ b/packages/types/src/context.ts @@ -12,6 +12,7 @@ export interface Contexts extends Record { trace?: TraceContext; cloud_resource?: CloudResourceContext; state?: StateContext; + profile?: ProfileContext; } export interface StateContext extends Record { @@ -114,3 +115,7 @@ export interface CloudResourceContext extends Record { ['host.id']?: string; ['host.type']?: string; } + +export interface ProfileContext extends Record { + profile_id: string; +} diff --git a/packages/types/src/envelope.ts b/packages/types/src/envelope.ts index 61d3b7d38a1d..d7089fbb0225 100644 --- a/packages/types/src/envelope.ts +++ b/packages/types/src/envelope.ts @@ -7,7 +7,7 @@ import type { FeedbackEvent, UserFeedback } from './feedback'; import type { Profile } from './profiling'; import type { ReplayEvent, ReplayRecordingData } from './replay'; import type { SdkInfo } from './sdkinfo'; -import type { SerializedSession, Session, SessionAggregates } from './session'; +import type { SerializedSession, SessionAggregates } from './session'; import type { SpanJSON } from './span'; // Based on: https://develop.sentry.dev/sdk/envelopes/ @@ -87,8 +87,7 @@ export type EventItem = BaseEnvelopeItem; export type AttachmentItem = BaseEnvelopeItem; export type UserFeedbackItem = BaseEnvelopeItem; export type SessionItem = - // TODO(v8): Only allow serialized session here (as opposed to Session or SerializedSesison) - | BaseEnvelopeItem + | BaseEnvelopeItem | BaseEnvelopeItem; export type ClientReportItem = BaseEnvelopeItem; export type CheckInItem = BaseEnvelopeItem; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index bb4230ea3e5c..c90b7841f9ff 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -164,6 +164,8 @@ export type { MetricsAggregator, MetricBucketItem, MetricInstance, + MetricData, + Metrics, } from './metrics'; export type { ParameterizedString } from './parameterize'; export type { ViewHierarchyData, ViewHierarchyWindow } from './view-hierarchy'; diff --git a/packages/types/src/metrics.ts b/packages/types/src/metrics.ts index 0f8dc4f53435..843068db0aef 100644 --- a/packages/types/src/metrics.ts +++ b/packages/types/src/metrics.ts @@ -1,6 +1,14 @@ +import type { Client } from './client'; import type { MeasurementUnit } from './measurement'; import type { Primitive } from './misc'; +export interface MetricData { + unit?: MeasurementUnit; + tags?: Record; + timestamp?: number; + client?: Client; +} + /** * An abstract definition of the minimum required API * for a metric instance. @@ -62,3 +70,33 @@ export interface MetricsAggregator { */ toString(): string; } + +export interface Metrics { + /** + * Adds a value to a counter metric + * + * @experimental This API is experimental and might have breaking changes in the future. + */ + increment(name: string, value?: number, data?: MetricData): void; + + /** + * Adds a value to a distribution metric + * + * @experimental This API is experimental and might have breaking changes in the future. + */ + distribution(name: string, value: number, data?: MetricData): void; + + /** + * Adds a value to a set metric. Value must be a string or integer. + * + * @experimental This API is experimental and might have breaking changes in the future. + */ + set(name: string, value: number | string, data?: MetricData): void; + + /** + * Adds a value to a gauge metric + * + * @experimental This API is experimental and might have breaking changes in the future. + */ + gauge(name: string, value: number, data?: MetricData): void; +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index a0649cef48ad..2fb6f420ab58 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -35,3 +35,4 @@ export * from './eventbuilder'; export * from './anr'; export * from './lru'; export * from './buildPolyfills'; +export * from './propagationContext'; diff --git a/packages/utils/src/propagationContext.ts b/packages/utils/src/propagationContext.ts new file mode 100644 index 000000000000..745531c8aa98 --- /dev/null +++ b/packages/utils/src/propagationContext.ts @@ -0,0 +1,12 @@ +import type { PropagationContext } from '@sentry/types'; +import { uuid4 } from './misc'; + +/** + * Returns a new minimal propagation context + */ +export function generatePropagationContext(): PropagationContext { + return { + traceId: uuid4(), + spanId: uuid4().substring(16), + }; +} diff --git a/packages/utils/test/proagationContext.test.ts b/packages/utils/test/proagationContext.test.ts new file mode 100644 index 000000000000..01c8569bde9b --- /dev/null +++ b/packages/utils/test/proagationContext.test.ts @@ -0,0 +1,10 @@ +import { generatePropagationContext } from '../src/propagationContext'; + +describe('generatePropagationContext', () => { + it('generates a new minimal propagation context', () => { + expect(generatePropagationContext()).toEqual({ + traceId: expect.stringMatching(/^[0-9a-f]{32}$/), + spanId: expect.stringMatching(/^[0-9a-f]{16}$/), + }); + }); +}); diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index ce4ef113908b..79c6d77c9d21 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -58,6 +58,7 @@ export { startSpan, startInactiveSpan, startSpanManual, + startNewTrace, withActiveSpan, getSpanDescendants, continueTrace, diff --git a/scripts/node-unit-tests.ts b/scripts/node-unit-tests.ts index bf46320334df..119f9764fc60 100644 --- a/scripts/node-unit-tests.ts +++ b/scripts/node-unit-tests.ts @@ -15,6 +15,7 @@ const DEFAULT_SKIP_TESTS_PACKAGES = [ '@sentry/vue', '@sentry/react', '@sentry/angular', + '@sentry/solidjs', '@sentry/svelte', '@sentry/profiling-node', '@sentry-internal/browser-utils', diff --git a/yarn.lock b/yarn.lock index 176a6abb92d7..52fbb7373cb6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1244,6 +1244,27 @@ json5 "^2.2.3" semver "^6.3.1" +"@babel/core@^7.23.3": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.5.tgz#15ab5b98e101972d171aeef92ac70d8d6718f06a" + integrity sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.24.2" + "@babel/generator" "^7.24.5" + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-module-transforms" "^7.24.5" + "@babel/helpers" "^7.24.5" + "@babel/parser" "^7.24.5" + "@babel/template" "^7.24.0" + "@babel/traverse" "^7.24.5" + "@babel/types" "^7.24.5" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + "@babel/core@^7.24.0", "@babel/core@^7.24.4": version "7.24.4" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.4.tgz#1f758428e88e0d8c563874741bc4ffc4f71a4717" @@ -1323,6 +1344,16 @@ "@jridgewell/trace-mapping" "^0.3.25" jsesc "^2.5.1" +"@babel/generator@^7.24.5": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.5.tgz#e5afc068f932f05616b66713e28d0f04e99daeb3" + integrity sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA== + dependencies: + "@babel/types" "^7.24.5" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@7.18.6", "@babel/helper-annotate-as-pure@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" @@ -1538,7 +1569,7 @@ dependencies: "@babel/types" "^7.23.0" -"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.18.6": +"@babel/helper-module-imports@7.18.6", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== @@ -1552,7 +1583,7 @@ dependencies: "@babel/types" "^7.22.15" -"@babel/helper-module-imports@^7.24.1": +"@babel/helper-module-imports@^7.24.1", "@babel/helper-module-imports@^7.24.3": version "7.24.3" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz#6ac476e6d168c7c23ff3ba3cf4f7841d46ac8128" integrity sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg== @@ -1595,6 +1626,17 @@ "@babel/helper-split-export-declaration" "^7.22.6" "@babel/helper-validator-identifier" "^7.22.20" +"@babel/helper-module-transforms@^7.24.5": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz#ea6c5e33f7b262a0ae762fd5986355c45f54a545" + integrity sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-module-imports" "^7.24.3" + "@babel/helper-simple-access" "^7.24.5" + "@babel/helper-split-export-declaration" "^7.24.5" + "@babel/helper-validator-identifier" "^7.24.5" + "@babel/helper-optimise-call-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe" @@ -1682,6 +1724,13 @@ dependencies: "@babel/types" "^7.22.5" +"@babel/helper-simple-access@^7.24.5": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz#50da5b72f58c16b07fbd992810be6049478e85ba" + integrity sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ== + dependencies: + "@babel/types" "^7.24.5" + "@babel/helper-skip-transparent-expression-wrappers@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.18.9.tgz#778d87b3a758d90b471e7b9918f34a9a02eb5818" @@ -1710,6 +1759,13 @@ dependencies: "@babel/types" "^7.22.5" +"@babel/helper-split-export-declaration@^7.24.5": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz#b9a67f06a46b0b339323617c8c6213b9055a78b6" + integrity sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q== + dependencies: + "@babel/types" "^7.24.5" + "@babel/helper-string-parser@^7.19.4": version "7.19.4" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" @@ -1730,6 +1786,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83" integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== +"@babel/helper-string-parser@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz#f99c36d3593db9540705d0739a1f10b5e20c696e" + integrity sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ== + "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": version "7.19.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" @@ -1740,6 +1801,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== +"@babel/helper-validator-identifier@^7.24.5": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz#918b1a7fa23056603506370089bd990d8720db62" + integrity sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA== + "@babel/helper-validator-option@^7.16.7", "@babel/helper-validator-option@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" @@ -1828,6 +1894,15 @@ "@babel/traverse" "^7.24.1" "@babel/types" "^7.24.0" +"@babel/helpers@^7.24.5": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.5.tgz#fedeb87eeafa62b621160402181ad8585a22a40a" + integrity sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q== + dependencies: + "@babel/template" "^7.24.0" + "@babel/traverse" "^7.24.5" + "@babel/types" "^7.24.5" + "@babel/highlight@^7.10.4", "@babel/highlight@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" @@ -1890,7 +1965,7 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.15.tgz#eec9f36d8eaf0948bb88c87a46784b5ee9fd0c89" integrity sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg== -"@babel/parser@^7.21.8": +"@babel/parser@^7.21.8", "@babel/parser@^7.24.5": version "7.24.5" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.5.tgz#4a4d5ab4315579e5398a82dcf636ca80c3392790" integrity sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg== @@ -2254,6 +2329,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" +"@babel/plugin-syntax-jsx@^7.18.6": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz#3f6ca04b8c841811dbc3c5c5f837934e0d626c10" + integrity sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-jsx@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz#a6b68e84fb76e759fc3b93e901876ffabbe1d918" @@ -3473,6 +3555,22 @@ debug "^4.3.1" globals "^11.1.0" +"@babel/traverse@^7.24.5": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.5.tgz#972aa0bc45f16983bf64aa1f877b2dd0eea7e6f8" + integrity sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA== + dependencies: + "@babel/code-frame" "^7.24.2" + "@babel/generator" "^7.24.5" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.24.5" + "@babel/parser" "^7.24.5" + "@babel/types" "^7.24.5" + debug "^4.3.1" + globals "^11.1.0" + "@babel/types@7.20.7", "@babel/types@^7.20.7": version "7.20.7" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.7.tgz#54ec75e252318423fc07fb644dc6a58a64c09b7f" @@ -3527,6 +3625,15 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" +"@babel/types@^7.24.5": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.5.tgz#7661930afc638a5383eb0c4aee59b74f38db84d7" + integrity sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ== + dependencies: + "@babel/helper-string-parser" "^7.24.1" + "@babel/helper-validator-identifier" "^7.24.5" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -7661,6 +7768,13 @@ resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== +"@solidjs/testing-library@0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@solidjs/testing-library/-/testing-library-0.8.5.tgz#97061b2286d8641bd43bf474e624c3bb47e486a6" + integrity sha512-L9TowCoqdRQGB8ikODh9uHXrYTjCUZseVUG0tIVa836//qeSqXP4m0BKG66v9Zp1y1wRxok5qUW97GwrtEBMcw== + dependencies: + "@testing-library/dom" "^9.3.1" + "@strictsoftware/typedoc-plugin-monorepo@^0.3.1": version "0.3.1" resolved "https://registry.yarnpkg.com/@strictsoftware/typedoc-plugin-monorepo/-/typedoc-plugin-monorepo-0.3.1.tgz#83a704bad2cf90a05f62f1c2587b0be09693a9a0" @@ -7725,6 +7839,20 @@ lz-string "^1.4.4" pretty-format "^27.0.2" +"@testing-library/dom@^9.3.1": + version "9.3.4" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.3.4.tgz#50696ec28376926fec0a1bf87d9dbac5e27f60ce" + integrity sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^5.0.1" + aria-query "5.1.3" + chalk "^4.1.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.5.0" + pretty-format "^27.0.2" + "@testing-library/react-hooks@^7.0.2": version "7.0.2" resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-7.0.2.tgz#3388d07f562d91e7f2431a4a21b5186062ecfee0" @@ -7814,6 +7942,11 @@ resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.1.tgz#78b5433344e2f92e8b306c06a5622c50c245bf6b" integrity sha512-S6oPal772qJZHoRZLFc/XoZW2gFvwXusYUmXPXkgxJLuEk2vOt7jc4Yo6z/vtI0EBkbPBVrJJ0B+prLIKiWqHg== +"@types/aria-query@^5.0.1": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" + integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== + "@types/array.prototype.flat@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@types/array.prototype.flat/-/array.prototype.flat-1.2.1.tgz#5433a141730f8e1d7a8e7486458ceb8144ee5edc" @@ -7846,6 +7979,17 @@ "@types/babel__template" "*" "@types/babel__traverse" "*" +"@types/babel__core@^7.20.4": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + "@types/babel__generator@*": version "7.6.2" resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.2.tgz#f3d71178e187858f7c45e30380f8f1b7415a12d8" @@ -8314,17 +8458,7 @@ dependencies: "@types/unist" "*" -"@types/history-4@npm:@types/history@4.7.8": - version "4.7.8" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" - integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== - -"@types/history-5@npm:@types/history@4.7.8": - version "4.7.8" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" - integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== - -"@types/history@*": +"@types/history-4@npm:@types/history@4.7.8", "@types/history-5@npm:@types/history@4.7.8", "@types/history@*": version "4.7.8" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== @@ -8690,15 +8824,7 @@ "@types/history" "^3" "@types/react" "*" -"@types/react-router-4@npm:@types/react-router@5.1.14": - version "5.1.14" - resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" - integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== - dependencies: - "@types/history" "*" - "@types/react" "*" - -"@types/react-router-5@npm:@types/react-router@5.1.14": +"@types/react-router-4@npm:@types/react-router@5.1.14", "@types/react-router-5@npm:@types/react-router@5.1.14": version "5.1.14" resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== @@ -10303,6 +10429,13 @@ argv@0.0.2: resolved "https://registry.yarnpkg.com/argv/-/argv-0.0.2.tgz#ecbd16f8949b157183711b1bda334f37840185ab" integrity sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas= +aria-query@5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" + integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== + dependencies: + deep-equal "^2.0.5" + aria-query@^5.0.0, aria-query@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.2.tgz#0b8a744295271861e1d933f8feca13f9b70cfdc1" @@ -10330,6 +10463,14 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= +array-buffer-byte-length@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" + integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== + dependencies: + call-bind "^1.0.5" + is-array-buffer "^3.0.4" + array-differ@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" @@ -10686,6 +10827,13 @@ available-typed-arrays@^1.0.2: dependencies: array-filter "^1.0.0" +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + axios@1.6.7, axios@^1.0.0, axios@^1.6.7: version "1.6.7" resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.7.tgz#7b48c2e27c96f9c68a2f8f31e2ab19f59b06b0a7" @@ -10849,6 +10997,17 @@ babel-plugin-jest-hoist@^27.5.1: "@types/babel__core" "^7.0.0" "@types/babel__traverse" "^7.0.6" +babel-plugin-jsx-dom-expressions@^0.37.20: + version "0.37.21" + resolved "https://registry.yarnpkg.com/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.37.21.tgz#8d63d09c183485f228a11f13cbdf1ff25e541a8e" + integrity sha512-WbQo1NQ241oki8bYasVzkMXOTSIri5GO/K47rYJb2ZBh8GaPUEWiWbMV3KwXz+96eU2i54N6ThzjQG/f5n8Azw== + dependencies: + "@babel/helper-module-imports" "7.18.6" + "@babel/plugin-syntax-jsx" "^7.18.6" + "@babel/types" "^7.20.7" + html-entities "2.3.3" + validate-html-nesting "^1.2.1" + babel-plugin-module-resolver@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/babel-plugin-module-resolver/-/babel-plugin-module-resolver-3.2.0.tgz#ddfa5e301e3b9aa12d852a9979f18b37881ff5a7" @@ -10993,6 +11152,13 @@ babel-preset-jest@^27.5.1: babel-plugin-jest-hoist "^27.5.1" babel-preset-current-node-syntax "^1.0.0" +babel-preset-solid@^1.8.4: + version "1.8.17" + resolved "https://registry.yarnpkg.com/babel-preset-solid/-/babel-preset-solid-1.8.17.tgz#8d55e8e2ee800be85527425e7943534f984dc815" + integrity sha512-s/FfTZOeds0hYxYqce90Jb+0ycN2lrzC7VP1k1JIn3wBqcaexDKdYi6xjB+hMNkL+Q6HobKbwsriqPloasR9LA== + dependencies: + babel-plugin-jsx-dom-expressions "^0.37.20" + backbone@^1.1.2: version "1.4.0" resolved "https://registry.yarnpkg.com/backbone/-/backbone-1.4.0.tgz#54db4de9df7c3811c3f032f34749a4cd27f3bd12" @@ -12194,6 +12360,17 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + callsites@^3.0.0, callsites@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -13521,6 +13698,11 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.7.tgz#2a5fb75e1015e84dd15692f71e89a1450290950b" integrity sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g== +csstype@^3.1.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + cuint@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b" @@ -13668,6 +13850,30 @@ deep-eql@^4.1.3: dependencies: type-detect "^4.0.0" +deep-equal@^2.0.5: + version "2.2.3" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1" + integrity sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA== + dependencies: + array-buffer-byte-length "^1.0.0" + call-bind "^1.0.5" + es-get-iterator "^1.1.3" + get-intrinsic "^1.2.2" + is-arguments "^1.1.1" + is-array-buffer "^3.0.2" + is-date-object "^1.0.5" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + isarray "^2.0.5" + object-is "^1.1.5" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.5.1" + side-channel "^1.0.4" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.13" + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -13702,6 +13908,15 @@ defer-to-connect@^1.0.1: resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + define-lazy-prop@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" @@ -13715,6 +13930,15 @@ define-properties@^1.1.3, define-properties@^1.1.4: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + define-property@^0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" @@ -15180,6 +15404,33 @@ es-check@7.1.0: supports-color "^8.1.1" winston "^3.8.2" +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-get-iterator@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" + integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + has-symbols "^1.0.3" + is-arguments "^1.1.1" + is-map "^2.0.2" + is-set "^2.0.2" + is-string "^1.0.7" + isarray "^2.0.5" + stop-iteration-iterator "^1.0.0" + es-module-lexer@^0.9.0: version "0.9.3" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" @@ -17038,6 +17289,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + function.prototype.name@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" @@ -17053,7 +17309,7 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= -functions-have-names@^1.2.2: +functions-have-names@^1.2.2, functions-have-names@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== @@ -17135,6 +17391,17 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1, get-intrinsic@ has "^1.0.3" has-symbols "^1.0.3" +get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" @@ -17356,15 +17623,15 @@ glob@^10.2.2: path-scurry "^1.10.0" glob@^10.3.10: - version "10.3.10" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" - integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== + version "10.4.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.1.tgz#0cfb01ab6a6b438177bfe6a58e2576f6efe909c2" + integrity sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw== dependencies: foreground-child "^3.1.0" - jackspeak "^2.3.5" - minimatch "^9.0.1" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - path-scurry "^1.10.1" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + path-scurry "^1.11.1" glob@^10.3.4: version "10.3.4" @@ -17598,6 +17865,13 @@ google-p12-pem@^3.0.3: dependencies: node-forge "^0.10.0" +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + got@^8.0.1: version "8.3.2" resolved "https://registry.yarnpkg.com/got/-/got-8.3.2.tgz#1d23f64390e97f776cac52e5b936e5f514d2e937" @@ -17759,6 +18033,18 @@ has-property-descriptors@^1.0.0: dependencies: get-intrinsic "^1.1.1" +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + has-symbol-support-x@^1.4.1: version "1.4.2" resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz#1409f98bc00247da45da67cee0a36f282ff26455" @@ -17783,6 +18069,13 @@ has-tostringtag@^1.0.0: dependencies: has-symbols "^1.0.2" +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + has-unicode@2.0.1, has-unicode@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -17860,6 +18153,13 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + hast-util-from-parse5@^7.0.0: version "7.1.2" resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-7.1.2.tgz#aecfef73e3ceafdfa4550716443e4eb7b02e22b0" @@ -18228,6 +18528,11 @@ html-encoding-sniffer@^3.0.0: dependencies: whatwg-encoding "^2.0.0" +html-entities@2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.3.tgz#117d7626bece327fc8baace8868fa6f5ef856e46" + integrity sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA== + html-entities@^2.3.2: version "2.5.2" resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f" @@ -18754,6 +19059,15 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +internal-slot@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" + integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.0" + side-channel "^1.0.4" + interpret@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" @@ -18825,6 +19139,22 @@ is-arguments@^1.0.4: dependencies: call-bind "^1.0.0" +is-arguments@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-array-buffer@^3.0.2, is-array-buffer@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" + integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -18937,6 +19267,13 @@ is-date-object@^1.0.1: resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== +is-date-object@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + is-descriptor@^0.1.0: version "0.1.6" resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" @@ -19058,6 +19395,11 @@ is-language-code@^3.1.0: dependencies: "@babel/runtime" "^7.14.0" +is-map@^2.0.2, is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + is-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" @@ -19197,6 +19539,11 @@ is-retry-allowed@^1.1.0: resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== +is-set@^2.0.2, is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + is-shared-array-buffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" @@ -19295,6 +19642,11 @@ is-url@^1.2.4: resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52" integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww== +is-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== + is-weakref@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" @@ -19302,11 +19654,24 @@ is-weakref@^1.0.2: dependencies: call-bind "^1.0.2" +is-weakset@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.3.tgz#e801519df8c0c43e12ff2834eead84ec9e624007" + integrity sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + is-what@^3.14.1: version "3.14.1" resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1" integrity sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA== +is-what@^4.1.8: + version "4.1.16" + resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.16.tgz#1ad860a19da8b4895ad5495da3182ce2acdd7a6f" + integrity sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A== + is-windows@^1.0.1, is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -19351,6 +19716,11 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isbinaryfile@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-5.0.0.tgz#034b7e54989dab8986598cbcea41f66663c65234" @@ -19494,10 +19864,10 @@ jackspeak@^2.0.3: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" -jackspeak@^2.3.5: - version "2.3.6" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" - integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== +jackspeak@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.1.2.tgz#eada67ea949c6b71de50f1b09c92a961897b90ab" + integrity sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ== dependencies: "@isaacs/cliui" "^8.0.2" optionalDependencies: @@ -21084,6 +21454,11 @@ lowercase-keys@^2.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== +lru-cache@^10.2.0: + version "10.2.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.2.tgz#48206bc114c1252940c41b25b41af5b545aca878" + integrity sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -21138,6 +21513,11 @@ lz-string@^1.4.4: resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= +lz-string@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" + integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== + madge@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/madge/-/madge-7.0.0.tgz#64b1762033b0f969caa7e5853004b6850e8430bb" @@ -21647,6 +22027,13 @@ meow@^8.1.2: type-fest "^0.18.0" yargs-parser "^20.2.3" +merge-anything@^5.1.7: + version "5.1.7" + resolved "https://registry.yarnpkg.com/merge-anything/-/merge-anything-5.1.7.tgz#94f364d2b0cf21ac76067b5120e429353b3525d7" + integrity sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ== + dependencies: + is-what "^4.1.8" + merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -22131,6 +22518,13 @@ minimatch@^9.0.0, minimatch@^9.0.1: dependencies: brace-expansion "^2.0.1" +minimatch@^9.0.4: + version "9.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" + integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== + dependencies: + brace-expansion "^2.0.1" + minimatch@~3.0.4: version "3.0.8" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1" @@ -22255,6 +22649,11 @@ minipass@^5.0.0: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.3.tgz#05ea638da44e475037ed94d1c7efcc76a25e1974" integrity sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg== +minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + minizlib@^2.1.1, minizlib@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" @@ -23480,6 +23879,14 @@ object-is@^1.0.1: call-bind "^1.0.2" define-properties "^1.1.3" +object-is@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" + integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -24265,6 +24672,14 @@ path-scurry@^1.10.1: lru-cache "^9.1.1 || ^10.0.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-scurry@^1.6.1: version "1.6.4" resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.6.4.tgz#020a9449e5382a4acb684f9c7e1283bc5695de66" @@ -24480,7 +24895,12 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= -pirates@^4.0.1, pirates@^4.0.4: +pirates@^4.0.1: + version "4.0.6" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" + integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== + +pirates@^4.0.4: version "4.0.5" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== @@ -24587,6 +25007,11 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + postcss-attribute-case-insensitive@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz#03d761b24afc04c09e757e92ff53716ae8ea2741" @@ -25698,7 +26123,7 @@ react-is@^18.0.0: dependencies: "@remix-run/router" "1.0.2" -"react-router-6@npm:react-router@6.3.0": +"react-router-6@npm:react-router@6.3.0", react-router@6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== @@ -25713,13 +26138,6 @@ react-router-dom@^6.2.2: history "^5.2.0" react-router "6.3.0" -react-router@6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" - integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== - dependencies: - history "^5.2.0" - react@^18.0.0: version "18.0.0" resolved "https://registry.yarnpkg.com/react/-/react-18.0.0.tgz#b468736d1f4a5891f38585ba8e8fb29f91c3cb96" @@ -26052,6 +26470,16 @@ regexp.prototype.flags@^1.4.3: define-properties "^1.1.3" functions-have-names "^1.2.2" +regexp.prototype.flags@^1.5.1: + version "1.5.2" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" + integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== + dependencies: + call-bind "^1.0.6" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.1" + regexpp@^3.1.0, regexpp@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" @@ -27074,6 +27502,16 @@ serialize-javascript@^6.0.1: dependencies: randombytes "^2.1.0" +seroval-plugins@^1.0.3: + version "1.0.7" + resolved "https://registry.yarnpkg.com/seroval-plugins/-/seroval-plugins-1.0.7.tgz#c02511a1807e9bc8f68a91fbec13474fa9cea670" + integrity sha512-GO7TkWvodGp6buMEX9p7tNyIkbwlyuAWbI6G9Ec5bhcm7mQdu3JOK1IXbEUwb3FVzSc363GraG/wLW23NSavIw== + +seroval@^1.0.3: + version "1.0.7" + resolved "https://registry.yarnpkg.com/seroval/-/seroval-1.0.7.tgz#ee48ad8ba69f1595bdd5c55d1a0d1da29dee7455" + integrity sha512-n6ZMQX5q0Vn19Zq7CIKNIo7E75gPkGCFUEqDpa8jgwpYr/vScjqnQ6H09t1uIiZ0ZSK0ypEGvrYK2bhBGWsGdw== + serve-index@^1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" @@ -27117,6 +27555,28 @@ set-cookie-parser@^2.6.0: resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz#131921e50f62ff1a66a461d7d62d7b21d5d15a51" integrity sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ== +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +set-function-name@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" @@ -27489,6 +27949,24 @@ socks@^2.6.2: ip "^2.0.0" smart-buffer "^4.2.0" +solid-js@1.8.11: + version "1.8.11" + resolved "https://registry.yarnpkg.com/solid-js/-/solid-js-1.8.11.tgz#0e7496a9834720b10fe739eaac250221d3f72cd5" + integrity sha512-WdwmER+TwBJiN4rVQTVBxocg+9pKlOs41KzPYntrC86xO5sek8TzBYozPEZPL1IRWDouf2lMrvSbIs3CanlPvQ== + dependencies: + csstype "^3.1.0" + seroval "^1.0.3" + seroval-plugins "^1.0.3" + +solid-refresh@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/solid-refresh/-/solid-refresh-0.6.3.tgz#d23ef80f04e177619c9234a809c573cb16360627" + integrity sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA== + dependencies: + "@babel/generator" "^7.23.6" + "@babel/helper-module-imports" "^7.22.15" + "@babel/types" "^7.23.6" + sorcery@0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/sorcery/-/sorcery-0.11.0.tgz#310c80ee993433854bb55bb9aa4003acd147fca8" @@ -27877,6 +28355,13 @@ stdin-discarder@^0.1.0: dependencies: bl "^5.0.0" +stop-iteration-iterator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" + integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== + dependencies: + internal-slot "^1.0.4" + stream-browserify@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" @@ -27971,7 +28456,7 @@ string-template@~0.2.1: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0= -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -27997,15 +28482,6 @@ string-width@^2.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -28101,14 +28577,7 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -28263,7 +28732,7 @@ stylus@0.59.0, stylus@^0.59.0: sax "~1.2.4" source-map "^0.7.3" -sucrase@^3.27.0: +sucrase@^3.27.0, sucrase@^3.35.0: version "3.35.0" resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.0.tgz#57f17a3d7e19b36d8995f06679d121be914ae263" integrity sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA== @@ -28682,7 +29151,7 @@ text-table@0.2.0, text-table@^0.2.0: thenify-all@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" - integrity sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY= + integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== dependencies: thenify ">= 3.1.0 < 4" @@ -29807,6 +30276,11 @@ v8-to-istanbul@^8.1.0: convert-source-map "^1.6.0" source-map "^0.7.3" +validate-html-nesting@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/validate-html-nesting/-/validate-html-nesting-1.2.2.tgz#2d74de14b598a0de671fad01bd71deabb93b8aca" + integrity sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg== + validate-npm-package-license@3.0.4, validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -29939,6 +30413,18 @@ vite-node@1.6.0: picocolors "^1.0.0" vite "^5.0.0" +vite-plugin-solid@^2.8.2: + version "2.10.2" + resolved "https://registry.yarnpkg.com/vite-plugin-solid/-/vite-plugin-solid-2.10.2.tgz#180f5ec9d8ac03d19160dd5728b313fe9b62ee0d" + integrity sha512-AOEtwMe2baBSXMXdo+BUwECC8IFHcKS6WQV/1NEd+Q7vHPap5fmIhLcAzr+DUJ04/KHx/1UBU0l1/GWP+rMAPQ== + dependencies: + "@babel/core" "^7.23.3" + "@types/babel__core" "^7.20.4" + babel-preset-solid "^1.8.4" + merge-anything "^5.1.7" + solid-refresh "^0.6.3" + vitefu "^0.2.5" + vite@4.5.3: version "4.5.3" resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.3.tgz#d88a4529ea58bae97294c7e2e6f0eab39a50fb1a" @@ -29983,7 +30469,7 @@ vite@^5.0.10: optionalDependencies: fsevents "~2.3.3" -vitefu@^0.2.2: +vitefu@^0.2.2, vitefu@^0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/vitefu/-/vitefu-0.2.5.tgz#c1b93c377fbdd3e5ddd69840ea3aa70b40d90969" integrity sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q== @@ -30531,6 +31017,16 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" +which-collection@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== + dependencies: + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" + which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" @@ -30557,6 +31053,17 @@ which-pm@^2.1.1: load-yaml-file "^0.2.0" path-exists "^4.0.0" +which-typed-array@^1.1.13: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" + which-typed-array@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.4.tgz#8fcb7d3ee5adf2d771066fba7cf37e32fe8711ff" @@ -30697,7 +31204,7 @@ workerpool@^6.4.0: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.4.0.tgz#f8d5cfb45fde32fa3b7af72ad617c3369567a462" integrity sha512-i3KR1mQMNwY2wx20ozq2EjISGtQWDIfV56We+yGJ5yDs8jTwQiLLaqHlkBHITlCuJnYlVRmXegxFxZg7gqI++A== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -30715,15 +31222,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"