From a9e6ba82a8eee4a6c21bd305ef5520b4444dba5a Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 24 May 2024 09:45:49 +0200 Subject: [PATCH 01/26] fix(node): Add origin to redis span (#12201) Noticed we forgot to set this. --- .../suites/tracing/redis-cache/test.ts | 5 +++++ .../node-integration-tests/suites/tracing/redis/test.ts | 4 ++++ packages/node/src/integrations/tracing/redis.ts | 3 +++ 3 files changed, 12 insertions(+) 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..de1f2eff3a53 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 @@ -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/packages/node/src/integrations/tracing/redis.ts b/packages/node/src/integrations/tracing/redis.ts index f9309b2de33e..a734d7cb864f 100644 --- a/packages/node/src/integrations/tracing/redis.ts +++ b/packages/node/src/integrations/tracing/redis.ts @@ -4,6 +4,7 @@ import { SEMANTIC_ATTRIBUTE_CACHE_ITEM_SIZE, SEMANTIC_ATTRIBUTE_CACHE_KEY, SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, defineIntegration, spanToJSON, } from '@sentry/core'; @@ -49,6 +50,8 @@ const _redisIntegration = ((options?: RedisOptions) => { 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; From 0ac519aa8c583268ae613d41c30106cd832968c8 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Fri, 24 May 2024 11:20:18 +0200 Subject: [PATCH 02/26] chore(ci): Remove unused NODE_VERSION 16 env from browser ci tests (#12203) The action sets up node 18 currently anyway, this doesn't seem to have any effect. --- .github/workflows/build.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9ef18fe35116..94cd5c28e07f 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 From a9cef35a46ab82751928519064842b9518b98d57 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 24 May 2024 06:00:42 -0400 Subject: [PATCH 03/26] fix(browser): Remove optional chaining in INP code (#12196) --- packages/browser-utils/src/metrics/inp.ts | 8 +++++++- packages/types/src/context.ts | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) 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/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; +} From 41951ba9fa810f3859cd263a7684dfcb84db0d45 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 24 May 2024 12:01:37 +0200 Subject: [PATCH 04/26] fix(nextjs): Don't report React postpone errors (#12194) --- .../nextjs-15/app/ppr-error/[param]/page.tsx | 18 +++++++++++++ .../nextjs-15/next.config.js | 6 ++++- .../nextjs-15/tests/ppr-error.test.ts | 22 +++++++++++++++ packages/nextjs/src/server/index.ts | 27 ++++++++++++++++++- 4 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15/app/ppr-error/[param]/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15/tests/ppr-error.test.ts 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/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index acfa4f03af97..00cc694f079a 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 { addEventProcessor, 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'; @@ -181,6 +181,31 @@ 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); } From 805c577b71d2f5b7299746e12250223681eaa71f Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 24 May 2024 12:17:00 +0200 Subject: [PATCH 05/26] build(react/remix): Use new `jsx` JSX transform instead of `React.createElement` (#12204) --- .size-limit.js | 2 + dev-packages/rollup-utils/bundleHelpers.mjs | 2 +- dev-packages/rollup-utils/npmHelpers.mjs | 2 +- .../rollup-utils/plugins/npmPlugins.mjs | 21 +++-- .../plugins/vendor/sucrase-plugin.mjs | 79 +++++++++++++++++++ package.json | 2 + packages/feedback/rollup.bundle.config.mjs | 6 ++ packages/feedback/rollup.npm.config.mjs | 2 + packages/react/package.json | 2 +- packages/react/rollup.npm.config.mjs | 7 ++ packages/remix/rollup.npm.config.mjs | 6 +- yarn.lock | 58 ++++++++++---- 12 files changed, 163 insertions(+), 26 deletions(-) create mode 100644 dev-packages/rollup-utils/plugins/vendor/sucrase-plugin.mjs diff --git a/.size-limit.js b/.size-limit.js index 1747b93aea21..295c4a2e1707 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -83,6 +83,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 +91,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/dev-packages/rollup-utils/bundleHelpers.mjs b/dev-packages/rollup-utils/bundleHelpers.mjs index ae6574c0b0bc..3bf6b38b3457 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); 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/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..20f81dc20f0a 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,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 +125,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/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/react/package.json b/packages/react/package.json index 50e15e27ae5c..4bee0dfc85ad 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" }, "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/yarn.lock b/yarn.lock index 176a6abb92d7..f42ff67439a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17356,15 +17356,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" @@ -19494,10 +19494,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 +21084,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" @@ -22131,6 +22136,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 +22267,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" @@ -24265,6 +24282,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 +24505,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== @@ -28263,7 +28293,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 +28712,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" From ca4afefdc9a13f05481442f3c7567ebe7ad21b32 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 24 May 2024 07:34:51 -0400 Subject: [PATCH 06/26] feat(react): Add React 19 to peer deps (#12207) Closes https://github.com/getsentry/sentry-javascript/issues/12200 From my personal testing everything works with with React 19, so we can bump the peer dep. For the new error APIs, there is https://github.com/getsentry/sentry-javascript/pull/12147, but that PR is draft while we get feedback from the React team. --- packages/react/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/package.json b/packages/react/package.json index 4bee0dfc85ad..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.14.0 || 17.x || 18.x" + "react": "^16.14.0 || 17.x || 18.x || 19.x" }, "devDependencies": { "@testing-library/react": "^13.0.0", From 5791a389f00262f574627aa9bc5e0c1a0ae4d33b Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 24 May 2024 13:45:38 +0200 Subject: [PATCH 07/26] fix(replay): Update matcher for hydration error detection to new React docs (#12209) --- .../src/coreHandlers/handleBeforeSendEvent.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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. From 3ea5373ac6c9e999ca47a8dc8d197f71c1729631 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 24 May 2024 13:45:42 +0200 Subject: [PATCH 08/26] fix(nextjs): Use global scope for generic event filters (#12205) --- packages/nextjs/src/server/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 00cc694f079a..2b5d7251186a 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -1,4 +1,4 @@ -import { addEventProcessor, applySdkMetadata, getClient, getGlobalScope } 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') { @@ -207,7 +207,7 @@ export function init(options: NodeOptions): void { ); if (process.env.NODE_ENV === 'development') { - addEventProcessor(devErrorSymbolicationEventProcessor); + getGlobalScope().addEventProcessor(devErrorSymbolicationEventProcessor); } DEBUG_BUILD && logger.log('SDK successfully initialized'); From 93c467d6a0babb02fc28ca325c50767cb924a25a Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 24 May 2024 09:58:52 -0400 Subject: [PATCH 09/26] test: Disable ember-release and embroider-optimized ember canary tests (#12210) resolves https://github.com/getsentry/sentry-javascript/issues/12176 Given the test apps are having issues, disable these tests for now. --- .github/workflows/canary.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From 186e98214f5ebc62bc9e71244c36bfe100fd1ebf Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Fri, 24 May 2024 20:29:29 +0200 Subject: [PATCH 10/26] feat(solidjs): Add solidjs SDK basic package (#12193) --- .../e2e-tests/verdaccio-config/config.yaml | 6 + package.json | 1 + packages/solidjs/.eslintrc.js | 6 + packages/solidjs/LICENSE | 21 + packages/solidjs/README.md | 9 + packages/solidjs/package.json | 81 +++ packages/solidjs/rollup.npm.config.mjs | 3 + packages/solidjs/src/index.ts | 3 + packages/solidjs/src/sdk.ts | 16 + packages/solidjs/test/sdk.test.ts | 32 + packages/solidjs/tsconfig.json | 7 + packages/solidjs/tsconfig.test.json | 12 + packages/solidjs/tsconfig.types.json | 10 + packages/solidjs/vite.config.ts | 14 + scripts/node-unit-tests.ts | 1 + yarn.lock | 590 ++++++++++++++++-- 16 files changed, 751 insertions(+), 61 deletions(-) create mode 100644 packages/solidjs/.eslintrc.js create mode 100644 packages/solidjs/LICENSE create mode 100644 packages/solidjs/README.md create mode 100644 packages/solidjs/package.json create mode 100644 packages/solidjs/rollup.npm.config.mjs create mode 100644 packages/solidjs/src/index.ts create mode 100644 packages/solidjs/src/sdk.ts create mode 100644 packages/solidjs/test/sdk.test.ts create mode 100644 packages/solidjs/tsconfig.json create mode 100644 packages/solidjs/tsconfig.test.json create mode 100644 packages/solidjs/tsconfig.types.json create mode 100644 packages/solidjs/vite.config.ts 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/package.json b/package.json index 20f81dc20f0a..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", 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/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 f42ff67439a9..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" @@ -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" @@ -21143,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" @@ -21652,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" @@ -23497,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" @@ -24617,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" @@ -25728,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== @@ -25743,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" @@ -26082,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" @@ -27104,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" @@ -27147,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" @@ -27519,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" @@ -27907,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" @@ -28001,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== @@ -28027,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" @@ -28131,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== @@ -29837,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" @@ -29969,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" @@ -30013,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== @@ -30561,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" @@ -30587,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" @@ -30727,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== @@ -30745,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" From 8fa393cde2001ca8f52008744c2ee2c138af16e5 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 24 May 2024 22:37:37 +0200 Subject: [PATCH 11/26] fix(node): Change import of `@prisma/instrumentation` to use default import (#12185) --- packages/node/src/integrations/tracing/prisma.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/node/src/integrations/tracing/prisma.ts b/packages/node/src/integrations/tracing/prisma.ts index 7652ea793530..c2874a89f19b 100644 --- a/packages/node/src/integrations/tracing/prisma.ts +++ b/packages/node/src/integrations/tracing/prisma.ts @@ -8,9 +8,14 @@ const _prismaIntegration = (() => { return { name: 'Prisma', setupOnce() { + 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; + addOpenTelemetryInstrumentation( // does not have a hook to adjust spans & add origin - new prismaInstrumentation.PrismaInstrumentation({}), + new EsmInteropPrismaInstrumentation({}), ); }, From cc1cb7bfd93679f796d23a3572563c6b1c905c30 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 27 May 2024 09:28:49 +0200 Subject: [PATCH 12/26] feat(node): Ensure manual OTEL setup works (#12214) Based on the great feedback in https://github.com/getsentry/sentry-javascript/discussions/12191, this does some small adjustments to ensure that you actually can use the Node SDK properly with a custom OTEL setup: ## 1. Ensure we do not run `validateOpenTelemetrySetup()` when `skipOpenTelemetrySetup` is configured. Today, this is impossible to fix because you need a client to create the sampler, and you need the sampler to satisfy the validation, but the validation runs in `init()`. So either you call init() before doing your manual setup, which means you get the warning, or you do the manual setup first, but then you can't actually add the sampler, and still get the warning. This change means that users that configure `skipOpenTelemetrySetup` can manually call `Sentry.validateOpenTelemetrySetup()` if they want to get the validation, else there will be no validation automtically. ## 2. Export `SentryContextManager` from `@sentry/node` This is easier to use than to use the primitive `wrapContextManagerClass` from `@sentry/opentelemetry`. ## 3. Ensure we always run `setupEventContextTrace`, not tied to `skipOpenTelemetrySetup` There is no reason to tie this together, this now just always runs in `init()` - it's just an event processor! --- .../scripts/consistentExports.ts | 2 ++ packages/node/src/index.ts | 2 ++ packages/node/src/sdk/init.ts | 14 +++++++++++--- packages/node/src/sdk/initOtel.ts | 4 +--- 4 files changed, 16 insertions(+), 6 deletions(-) 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..138ea18b5e3d 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,8 @@ const NODE_EXPORTS_IGNORE = [ 'setNodeAsyncContextStrategy', 'getDefaultIntegrationsWithoutPerformance', 'initWithoutDefaultIntegrations', + 'SentryContextManager', + 'validateOpenTelemetrySetup', ]; const nodeExports = Object.keys(SentryNode).filter(e => !NODE_EXPORTS_IGNORE.includes(e)); diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 5cc16772189b..590ad7e82923 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -26,11 +26,13 @@ export { koaIntegration, setupKoaErrorHandler } from './integrations/tracing/koa export { connectIntegration, setupConnectErrorHandler } from './integrations/tracing/connect'; export { spotlightIntegration } from './integrations/spotlight'; +export { SentryContextManager } from './otel/contextManager'; export { init, getDefaultIntegrations, getDefaultIntegrationsWithoutPerformance, initWithoutDefaultIntegrations, + validateOpenTelemetrySetup, } from './sdk/init'; export { initOpenTelemetry } from './sdk/initOtel'; export { getAutoPerformanceIntegrations } from './integrations/tracing'; diff --git a/packages/node/src/sdk/init.ts b/packages/node/src/sdk/init.ts index 82cd26492960..83533842e76c 100644 --- a/packages/node/src/sdk/init.ts +++ b/packages/node/src/sdk/init.ts @@ -11,7 +11,11 @@ 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, @@ -196,12 +200,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..2556b86162b5 100644 --- a/packages/node/src/sdk/initOtel.ts +++ b/packages/node/src/sdk/initOtel.ts @@ -7,7 +7,7 @@ import { SEMRESATTRS_SERVICE_VERSION, } from '@opentelemetry/semantic-conventions'; import { SDK_VERSION } from '@sentry/core'; -import { SentryPropagator, SentrySampler, SentrySpanProcessor, setupEventContextTrace } from '@sentry/opentelemetry'; +import { SentryPropagator, SentrySampler, SentrySpanProcessor } from '@sentry/opentelemetry'; import { logger } from '@sentry/utils'; import { SentryContextManager } from '../otel/contextManager'; @@ -28,8 +28,6 @@ export function initOpenTelemetry(client: NodeClient): void { diag.setLogger(otelLogger, DiagLogLevel.DEBUG); } - setupEventContextTrace(client); - const provider = setupOtel(client); client.traceProvider = provider; } From 5a1fae6e575963799b6cf3df409ee2194b336fe7 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 27 May 2024 09:37:19 +0200 Subject: [PATCH 13/26] feat(core): Allow to pass custom scope to `captureFeedback()` (#12216) This allows to pass a custom scope to `Sentry.captureFeedback()`, like this: ```js Sentry.captureFeedback({message : 'test' }, {}, scope); ``` This should fix the use case from https://github.com/getsentry/sentry-javascript/issues/11072#issuecomment-2129259591 --- packages/core/src/feedback.ts | 7 ++-- packages/core/test/lib/feedback.test.ts | 51 ++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 4 deletions(-) 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/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); + }); }); From 330750dc50725d68d3793aa3bce06dbafdcd4890 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 27 May 2024 09:51:40 +0200 Subject: [PATCH 14/26] feat(core): Add `startNewTrace` API (#12138) Add a new `Sentry.startNewTrace` function that allows users to start a trace in isolation of a potentially still active trace. When this function is called, a new trace will be started on a forked scope which remains valid throughout the callback lifetime. --- .../trace-lifetime/startNewTrace/subject.js | 15 +++ .../startNewTrace/template.html | 10 ++ .../trace-lifetime/startNewTrace/test.ts | 106 ++++++++++++++++++ packages/astro/src/index.server.ts | 1 + packages/aws-serverless/src/index.ts | 1 + .../index.bundle.tracing.replay.feedback.ts | 1 + .../src/index.bundle.tracing.replay.ts | 1 + packages/browser/src/index.bundle.tracing.ts | 1 + packages/browser/src/index.ts | 1 + .../src/tracing/browserTracingIntegration.ts | 11 +- packages/bun/src/index.ts | 1 + packages/core/src/scope.ts | 9 +- packages/core/src/tracing/index.ts | 1 + packages/core/src/tracing/trace.ts | 27 ++++- packages/core/test/lib/tracing/trace.test.ts | 30 +++++ packages/deno/src/index.ts | 1 + packages/google-cloud-serverless/src/index.ts | 1 + packages/node/src/index.ts | 1 + packages/remix/src/index.server.ts | 1 + packages/sveltekit/src/server/index.ts | 1 + packages/utils/src/index.ts | 1 + packages/utils/src/propagationContext.ts | 12 ++ packages/utils/test/proagationContext.test.ts | 10 ++ packages/vercel-edge/src/index.ts | 1 + 24 files changed, 227 insertions(+), 18 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/test.ts create mode 100644 packages/utils/src/propagationContext.ts create mode 100644 packages/utils/test/proagationContext.test.ts 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/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/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/src/index.bundle.tracing.replay.feedback.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.ts index de8453db8784..b9dc457640be 100644 --- a/packages/browser/src/index.bundle.tracing.replay.feedback.ts +++ b/packages/browser/src/index.bundle.tracing.replay.feedback.ts @@ -10,6 +10,7 @@ export { 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..a0e4d4736384 100644 --- a/packages/browser/src/index.bundle.tracing.replay.ts +++ b/packages/browser/src/index.bundle.tracing.replay.ts @@ -10,6 +10,7 @@ export { 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..d540ff0bd6f9 100644 --- a/packages/browser/src/index.bundle.tracing.ts +++ b/packages/browser/src/index.bundle.tracing.ts @@ -11,6 +11,7 @@ export { startSpan, startInactiveSpan, startSpanManual, + startNewTrace, withActiveSpan, getSpanDescendants, setMeasurement, diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 86e6ea20fe81..245eaa966859 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -59,6 +59,7 @@ export { startInactiveSpan, startSpanManual, withActiveSpan, + startNewTrace, getSpanDescendants, setMeasurement, getSpanStatusFromHttpCode, 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/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/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/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/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/node/src/index.ts b/packages/node/src/index.ts index 590ad7e82923..85eb73b26a48 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -113,6 +113,7 @@ export { startSpan, startSpanManual, startInactiveSpan, + startNewTrace, getActiveSpan, withActiveSpan, getRootSpan, 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/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/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, From b188e61da708dc341d19d468fb0f005105fe7bfa Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 27 May 2024 09:52:32 +0200 Subject: [PATCH 15/26] feat(node): Add `@sentry/node/preload` hook (#12213) This PR adds a new way to initialize `@sentry/node`, which allows to use the SDK with performance instrumentation even if you cannot (for whatever reason) call `Sentry.init()` at the very start of your app. ## CJS usage In CommonJS mode, you can run the SDK like this: ```bash node --require @sentry/node/preload ./app.js ``` ```js // app.js const express = require('express'); const Sentry = require('@sentry/node'); const dsn = await getSentryDsn(); Sentry.init({ dsn }); // express is instrumented even though we initialized Sentry late ``` ## ESM usage in ESM mode, you can run the SDK like this: ```bash node --import @sentry/node/preload ./app.mjs ``` ```js // app.mjs import express from 'express'; import * as Sentry from '@sentry/node'; const dsn = await getSentryDsn(); Sentry.init({ dsn }); // express is instrumented even though we initialized Sentry late ``` ## Configuration options This script will by default preload all opentelemetry instrumentation. You can choose to instrument only specific packages like this: ```bash SENTRY_PRELOAD_INTEGRATIONS="Http,Express,Graphql" --import @sentry/node/preload ./app.mjs ``` You can also enable debug logging for the script via `SENTRY_DEBUG=true`. ## Manually preloading It is also possible to manually call `preloadOpenTelemetry()` to achieve the same thing. For example, in a CJS app you could do the following thing if you want to initialize late but don't want to use `--require`: ```js // preload.js const Sentry = require('@sentry/node'); Sentry.preloadOpenTelemetry(); // app.js // call this first, before any other requires! require('./preload.js'); // Then, other stuff const express = require('express'); const Sentry = require('@sentry/node'); const dsn = await getSentryDsn(); Sentry.init({ dsn }); ``` --- .github/workflows/build.yml | 2 + .../scripts/consistentExports.ts | 1 + .../node-express-cjs-preload/.npmrc | 2 + .../node-express-cjs-preload/package.json | 24 ++ .../playwright.config.mjs | 69 ++++++ .../node-express-cjs-preload/src/app.js | 52 +++++ .../start-event-proxy.mjs | 6 + .../tests/server.test.ts | 123 ++++++++++ .../node-express-esm-preload/.npmrc | 2 + .../node-express-esm-preload/package.json | 24 ++ .../playwright.config.mjs | 69 ++++++ .../node-express-esm-preload/src/app.mjs | 52 +++++ .../start-event-proxy.mjs | 6 + .../tests/server.test.ts | 123 ++++++++++ .../suites/tracing/redis-cache/test.ts | 2 +- packages/node/package.json | 8 + packages/node/rollup.npm.config.mjs | 2 +- packages/node/src/index.ts | 4 +- packages/node/src/init.ts | 2 +- packages/node/src/integrations/http.ts | 217 ++++++++++-------- .../node/src/integrations/tracing/connect.ts | 10 +- .../node/src/integrations/tracing/express.ts | 80 ++++--- .../node/src/integrations/tracing/fastify.ts | 24 +- .../node/src/integrations/tracing/graphql.ts | 39 ++-- .../src/integrations/tracing/hapi/index.ts | 10 +- .../node/src/integrations/tracing/index.ts | 48 +++- packages/node/src/integrations/tracing/koa.ts | 83 +++---- .../node/src/integrations/tracing/mongo.ts | 24 +- .../node/src/integrations/tracing/mongoose.ts | 24 +- .../node/src/integrations/tracing/mysql.ts | 10 +- .../node/src/integrations/tracing/mysql2.ts | 24 +- .../node/src/integrations/tracing/nest.ts | 11 +- .../node/src/integrations/tracing/postgres.ts | 26 ++- .../node/src/integrations/tracing/prisma.ts | 25 +- .../node/src/integrations/tracing/redis.ts | 100 ++++---- packages/node/src/otel/instrument.ts | 31 +++ packages/node/src/preload.ts | 19 ++ packages/node/src/sdk/{init.ts => index.ts} | 32 +-- packages/node/src/sdk/initOtel.ts | 103 ++++++++- packages/node/test/helpers/mockSdkInit.ts | 2 +- packages/node/test/sdk/init.test.ts | 2 +- packages/node/test/sdk/preload.test.ts | 50 ++++ 42 files changed, 1205 insertions(+), 362 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/node-express-cjs-preload/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/node-express-cjs-preload/package.json create mode 100644 dev-packages/e2e-tests/test-applications/node-express-cjs-preload/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-express-cjs-preload/src/app.js create mode 100644 dev-packages/e2e-tests/test-applications/node-express-cjs-preload/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-express-cjs-preload/tests/server.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-express-esm-preload/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/node-express-esm-preload/package.json create mode 100644 dev-packages/e2e-tests/test-applications/node-express-esm-preload/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-express-esm-preload/src/app.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-express-esm-preload/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-express-esm-preload/tests/server.test.ts create mode 100644 packages/node/src/otel/instrument.ts create mode 100644 packages/node/src/preload.ts rename packages/node/src/sdk/{init.ts => index.ts} (87%) create mode 100644 packages/node/test/sdk/preload.test.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 94cd5c28e07f..dd827c293a61 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1003,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/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 138ea18b5e3d..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 @@ -20,6 +20,7 @@ const NODE_EXPORTS_IGNORE = [ '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/node-integration-tests/suites/tracing/redis-cache/test.ts b/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts index de1f2eff3a53..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(); }); diff --git a/packages/node/package.json b/packages/node/package.json index c5cea8979ef3..4021baaa3fc8 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -49,6 +49,14 @@ "require": { "default": "./build/cjs/init.js" } + }, + "./preload": { + "import": { + "default": "./build/esm/preload.js" + }, + "require": { + "default": "./build/cjs/preload.js" + } } }, "typesVersions": { diff --git a/packages/node/rollup.npm.config.mjs b/packages/node/rollup.npm.config.mjs index 8e18333836ef..e0483c673d1c 100644 --- a/packages/node/rollup.npm.config.mjs +++ b/packages/node/rollup.npm.config.mjs @@ -19,7 +19,7 @@ export default [ localVariablesWorkerConfig, ...makeNPMConfigVariants( makeBaseNPMConfig({ - entrypoints: ['src/index.ts', 'src/init.ts'], + entrypoints: ['src/index.ts', 'src/init.ts', 'src/preload.ts'], packageSpecificConfig: { output: { // set exports to 'named' or 'auto' so that rollup doesn't warn diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 85eb73b26a48..0003850d8f71 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -33,8 +33,8 @@ export { getDefaultIntegrationsWithoutPerformance, initWithoutDefaultIntegrations, validateOpenTelemetrySetup, -} from './sdk/init'; -export { initOpenTelemetry } from './sdk/initOtel'; +} from './sdk'; +export { initOpenTelemetry, preloadOpenTelemetry } from './sdk/initOtel'; export { getAutoPerformanceIntegrations } from './integrations/tracing'; export { getSentryRelease, defaultStackParser } from './sdk/api'; export { createGetModuleFromFilename } from './utils/module'; diff --git a/packages/node/src/init.ts b/packages/node/src/init.ts index 245ae8573afa..3d4ba2ceff90 100644 --- a/packages/node/src/init.ts +++ b/packages/node/src/init.ts @@ -1,4 +1,4 @@ -import { init } from './sdk/init'; +import { init } from './sdk'; /** * The @sentry/node/init export can be used with the node --import and --require args to initialize the SDK entirely via diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 6854751fdcac..c135c32816a2 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -22,6 +22,8 @@ import type { HTTPModuleRequestIncomingMessage } from '../transports/http-module import { addOriginToSpan } from '../utils/addOriginToSpan'; import { getRequestUrl } from '../utils/getRequestUrl'; +const INTEGRATION_NAME = 'Http'; + interface HttpOptions { /** * Whether breadcrumbs should be recorded for requests. @@ -45,106 +47,127 @@ interface HttpOptions { _instrumentation?: typeof HttpInstrumentation; } -const _httpIntegration = ((options: HttpOptions = {}) => { - 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/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 c2874a89f19b..e5d9e61a0229 100644 --- a/packages/node/src/integrations/tracing/prisma.ts +++ b/packages/node/src/integrations/tracing/prisma.ts @@ -1,22 +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() { - 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; - - addOpenTelemetryInstrumentation( - // does not have a hook to adjust spans & add origin - new EsmInteropPrismaInstrumentation({}), - ); + instrumentPrisma(); }, setup(client) { diff --git a/packages/node/src/integrations/tracing/redis.ts b/packages/node/src/integrations/tracing/redis.ts index a734d7cb864f..1379336412f6 100644 --- a/packages/node/src/integrations/tracing/redis.ts +++ b/packages/node/src/integrations/tracing/redis.ts @@ -8,8 +8,8 @@ import { 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)); @@ -41,56 +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'); - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.redis'); + if (!_redisOptions?.cachePrefixes || !shouldConsiderForCache(redisCommand, key, _redisOptions.cachePrefixes)) { + // not relevant for cache + return; + } - if (!options?.cachePrefixes || !shouldConsiderForCache(redisCommand, key, options.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 87% rename from packages/node/src/sdk/init.ts rename to packages/node/src/sdk/index.ts index 83533842e76c..f149a44c06a0 100644 --- a/packages/node/src/sdk/init.ts +++ b/packages/node/src/sdk/index.ts @@ -18,7 +18,6 @@ import { } from '@sentry/opentelemetry'; import type { Client, Integration, Options } from '@sentry/types'; import { - GLOBAL_OBJ, consoleSandbox, dropUndefinedKeys, logger, @@ -30,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'; @@ -44,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()] : []; @@ -96,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. */ @@ -134,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(); diff --git a/packages/node/src/sdk/initOtel.ts b/packages/node/src/sdk/initOtel.ts index 2556b86162b5..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'; @@ -8,30 +9,98 @@ import { } from '@opentelemetry/semantic-conventions'; import { SDK_VERSION } from '@sentry/core'; import { SentryPropagator, SentrySampler, SentrySpanProcessor } from '@sentry/opentelemetry'; -import { logger } from '@sentry/utils'; +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); - }, - }); - - diag.setLogger(otelLogger, DiagLogLevel.DEBUG); + 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; + + if (debug) { + logger.enable(); + setupOpenTelemetryLogger(); + } + + if (!isCjs()) { + maybeInitializeEsmLoader(); + } + + // 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. */ export function setupOtel(client: NodeClient): BasicTracerProvider { // Create and configure NodeTracerProvider @@ -54,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'); + }); +}); From 5469f89588327692832d0c17a730367c98aa959d Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 27 May 2024 11:09:53 +0200 Subject: [PATCH 16/26] feat(lforst): Only allow `SerializedSession` in session envelope items (#11979) Co-authored-by: Francesco Novy --- packages/types/src/envelope.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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; From 84796988d4684dec5fd75f6d6781b9c3e8b66af8 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 27 May 2024 11:17:30 +0200 Subject: [PATCH 17/26] build: Add size-limit entry for metrics (#12227) We had no entry covering the bundle size of metrics so far. --- .size-limit.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.size-limit.js b/.size-limit.js index 295c4a2e1707..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', From 3e179e1936cc5d36943f8861091e31a9e7128149 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 27 May 2024 11:20:13 +0200 Subject: [PATCH 18/26] test(e2e): Unflake sveltekit test (#12228) --- .../sveltekit-2-svelte-5/test/performance.test.ts | 4 ++-- .../test-applications/sveltekit-2/test/performance.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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([ From 2043f2d575abdbfb027cf39ce1342f11e2c8670a Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 27 May 2024 11:48:56 +0200 Subject: [PATCH 19/26] feat(nextjs): Use Vercel's `waitUntil` to defer freezing of Vercel Lambdas (#12133) Co-authored-by: Francesco Novy --- packages/nextjs/src/common/_error.ts | 7 +++--- packages/nextjs/src/common/types.ts | 5 ---- .../src/common/utils/edgeWrapperUtils.ts | 9 ++++--- .../common/utils/platformSupportsStreaming.ts | 1 - .../nextjs/src/common/utils/responseEnd.ts | 8 +++++-- .../src/common/utils/vercelWaitUntil.ts | 21 ++++++++++++++++ .../nextjs/src/common/utils/wrapperUtils.ts | 16 ++++++------- .../common/withServerActionInstrumentation.ts | 15 +++--------- .../src/common/wrapApiHandlerWithSentry.ts | 24 ++++--------------- .../src/common/wrapRouteHandlerWithSentry.ts | 10 +++----- .../common/wrapServerComponentWithSentry.ts | 8 +++---- .../pages/api/doubleEndMethodOnVercel.ts | 2 -- 12 files changed, 57 insertions(+), 69 deletions(-) delete mode 100644 packages/nextjs/src/common/utils/platformSupportsStreaming.ts create mode 100644 packages/nextjs/src/common/utils/vercelWaitUntil.ts 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/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 Date: Mon, 27 May 2024 13:31:48 +0200 Subject: [PATCH 20/26] fix(aws-serverless): Avoid minifying `Module._resolveFilename` in Lambda layer bundle (#12232) FIx one of the two issues that currently cause our lambda layer to break. When we build the lambda layer bundle we applied minification via our terser plugin which mangles `_`-prefixed private variables - like `Module._resolveFilename`. This patch adds the method to the list of excluded `_`-prefixed symbols. --- dev-packages/rollup-utils/plugins/bundlePlugins.mjs | 2 ++ 1 file changed, 2 insertions(+) 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', ], }, }, From 1f8f1b8128e4b1f4ea83031ab94ad351822cf5de Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 27 May 2024 08:51:46 -0400 Subject: [PATCH 21/26] ref(profiling-node): Add warning when using non-LTS node (#12211) resolves https://github.com/getsentry/sentry-javascript/issues/11777 --- packages/profiling-node/src/integration.ts | 10 ++++++++++ packages/profiling-node/src/nodeVersion.ts | 4 ++++ 2 files changed, 14 insertions(+) create mode 100644 packages/profiling-node/src/nodeVersion.ts 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; From 5282753dc291a8b628fe5a5942ed49a9c39d3b09 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 27 May 2024 14:52:14 +0200 Subject: [PATCH 22/26] fix: Only import `inspector` asynchronously (#12231) Closes #12223 I've added a test that uses `import-in-the-middle` to throw if `inspector` is imported without enabling the `LocalVariables` integration so this should not regress in the future. Note that we don't need to import asynchronously in the worker scripts since these are not evaluated if the features aren't used. --- .../LocalVariables/deny-inspector.mjs | 13 ++ .../suites/public-api/LocalVariables/test.ts | 8 + packages/node/src/integrations/anr/index.ts | 2 +- packages/node/src/integrations/anr/worker.ts | 2 +- .../local-variables/local-variables-async.ts | 2 +- .../local-variables/local-variables-sync.ts | 218 +++++++++--------- 6 files changed, 135 insertions(+), 110 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/public-api/LocalVariables/deny-inspector.mjs 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/packages/node/src/integrations/anr/index.ts b/packages/node/src/integrations/anr/index.ts index 671aae2bad49..92a4078aa766 100644 --- a/packages/node/src/integrations/anr/index.ts +++ b/packages/node/src/integrations/anr/index.ts @@ -1,4 +1,3 @@ -import * as inspector from 'node:inspector'; import { Worker } from 'node:worker_threads'; import { defineIntegration, getCurrentScope, getGlobalScope, getIsolationScope, mergeScopeData } from '@sentry/core'; import type { Contexts, Event, EventHint, Integration, IntegrationFn, ScopeData } from '@sentry/types'; @@ -148,6 +147,7 @@ async function _startWorker( }; if (options.captureStackTrace) { + const inspector = await import('node:inspector'); if (!inspector.url()) { inspector.open(0); } diff --git a/packages/node/src/integrations/anr/worker.ts b/packages/node/src/integrations/anr/worker.ts index 6f701919c531..67532435a39e 100644 --- a/packages/node/src/integrations/anr/worker.ts +++ b/packages/node/src/integrations/anr/worker.ts @@ -1,3 +1,4 @@ +import { Session as InspectorSession } from 'node:inspector'; import { parentPort, workerData } from 'node:worker_threads'; import { applyScopeDataToEvent, @@ -15,7 +16,6 @@ import { uuid4, watchdogTimer, } from '@sentry/utils'; -import { Session as InspectorSession } from 'inspector'; import { makeNodeTransport } from '../../transports'; import { createGetModuleFromFilename } from '../../utils/module'; 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) { From 83c255a41b36d8083b09f66b9f813f09d93c75c9 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 27 May 2024 15:58:48 +0200 Subject: [PATCH 23/26] fix(aws): Ensure lambda layer uses default export from `ImportInTheMiddle` (#12233) Our lambda layer relies on one bundled Sentry SDK, which caused problems raised in #12089 and https://github.com/getsentry/sentry-javascript/issues/12009#issuecomment-2126211967. Specifically, by bundling `import-in-the-middle` code into one file, it seems like the library's way of declaring its exports conflict, causing the "ImportInTheMiddle is not a constructor" error to be thrown. While this should ideally be fixed soon in `import-in-the-middle`, for now this patch adds a small Rollup plugin to transform `new ImportInTheMiddle(...)` calls to `new ImportInTheMiddle.default(...)` to our AWS Lambda Layer bundle config. --- dev-packages/rollup-utils/bundleHelpers.mjs | 22 ++++++++++++++++--- packages/aws-serverless/rollup.aws.config.mjs | 3 +-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/dev-packages/rollup-utils/bundleHelpers.mjs b/dev-packages/rollup-utils/bundleHelpers.mjs index 3bf6b38b3457..9e507f7f65fc 100644 --- a/dev-packages/rollup-utils/bundleHelpers.mjs +++ b/dev-packages/rollup-utils/bundleHelpers.mjs @@ -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/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 From 8dca102eb70b9f79a6c752b5fbcb75ec6a154195 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 27 May 2024 16:05:32 +0200 Subject: [PATCH 24/26] fix(browser): Improve browser extension error message check (#12146) Make our check for when we abort and log an error due to `Sentry.init` being used in a browser extension a bit more fine-gained. In particular, we now do not abort the SDK initialization if we detect that the SDK is running in a browser-extension dedicated window (e.g. a URL starting with `chrome-extension://`). --- packages/browser/src/sdk.ts | 40 ++++++++++++++++---------- packages/browser/test/unit/sdk.test.ts | 31 ++++++++++++++++++-- 2 files changed, 53 insertions(+), 18 deletions(-) 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/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); From 0fbe39adc0293489019fe661fc0754f2f58d838e Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 27 May 2024 17:19:10 +0200 Subject: [PATCH 25/26] feat(browser): Do not include metrics in base CDN bundle (#12230) Metrics are only included when performance is included, reducing the base bundle size. We always expose a shim so there is no API breakage, it just does nothing. I noticed this while working on https://github.com/getsentry/sentry-javascript/pull/12226. --- .../suites/metrics/{ => metricsEvent}/init.js | 0 .../suites/metrics/{ => metricsEvent}/test.ts | 12 +++++- .../suites/metrics/metricsShim/init.js | 13 +++++++ .../suites/metrics/metricsShim/test.ts | 36 ++++++++++++++++++ .../utils/helpers.ts | 14 ++++++- packages/browser/src/exports.ts | 2 - packages/browser/src/index.bundle.feedback.ts | 3 +- packages/browser/src/index.bundle.replay.ts | 7 +++- .../index.bundle.tracing.replay.feedback.ts | 2 + .../src/index.bundle.tracing.replay.ts | 2 + packages/browser/src/index.bundle.tracing.ts | 2 + packages/browser/src/index.bundle.ts | 2 + packages/browser/src/index.ts | 2 + packages/browser/src/metrics.ts | 4 +- packages/core/src/index.ts | 2 +- packages/core/src/metrics/exports-default.ts | 8 ++-- packages/core/src/metrics/exports.ts | 14 +------ packages/integration-shims/src/index.ts | 1 + packages/integration-shims/src/metrics.ts | 16 ++++++++ packages/types/src/index.ts | 2 + packages/types/src/metrics.ts | 38 +++++++++++++++++++ 21 files changed, 156 insertions(+), 26 deletions(-) rename dev-packages/browser-integration-tests/suites/metrics/{ => metricsEvent}/init.js (100%) rename dev-packages/browser-integration-tests/suites/metrics/{ => metricsEvent}/test.ts (73%) create mode 100644 dev-packages/browser-integration-tests/suites/metrics/metricsShim/init.js create mode 100644 dev-packages/browser-integration-tests/suites/metrics/metricsShim/test.ts create mode 100644 packages/integration-shims/src/metrics.ts 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/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/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 b9dc457640be..29438387ee5b 100644 --- a/packages/browser/src/index.bundle.tracing.replay.feedback.ts +++ b/packages/browser/src/index.bundle.tracing.replay.feedback.ts @@ -4,6 +4,8 @@ registerSpanErrorInstrumentation(); export * from './index.bundle.base'; +export * from './metrics'; + export { getActiveSpan, getRootSpan, diff --git a/packages/browser/src/index.bundle.tracing.replay.ts b/packages/browser/src/index.bundle.tracing.replay.ts index a0e4d4736384..6520aa185b2f 100644 --- a/packages/browser/src/index.bundle.tracing.replay.ts +++ b/packages/browser/src/index.bundle.tracing.replay.ts @@ -4,6 +4,8 @@ registerSpanErrorInstrumentation(); export * from './index.bundle.base'; +export * from './metrics'; + export { getActiveSpan, getRootSpan, diff --git a/packages/browser/src/index.bundle.tracing.ts b/packages/browser/src/index.bundle.tracing.ts index d540ff0bd6f9..8115e628aa89 100644 --- a/packages/browser/src/index.bundle.tracing.ts +++ b/packages/browser/src/index.bundle.tracing.ts @@ -5,6 +5,8 @@ registerSpanErrorInstrumentation(); export * from './index.bundle.base'; +export * from './metrics'; + export { getActiveSpan, getRootSpan, 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 245eaa966859..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, 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/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/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/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; +} From f32f88d25f00aa54161751229195994c4551e850 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 27 May 2024 16:18:02 +0200 Subject: [PATCH 26/26] meta: Add CHANGELOG entry for `8.5.0` --- CHANGELOG.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) 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