diff --git a/CHANGELOG.md b/CHANGELOG.md index 28161bfb0ecc..ab411b85a506 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,35 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.96.0 + +### Important Changes + +#### Deprecations + +This release includes some deprecations for integrations in `@sentry/browser` and frontend framework SDKs +(`@sentry/react`, `@sentry/vue`, etc.). Please take a look at our +[migration guide](https://github.com/getsentry/sentry-javascript/blob/develop/MIGRATION.md) for more details. + +- feat(browser): Export functional integrations & deprecate classes (#10267) + +#### Web Vitals Fix for LCP and CLS + +This release fixes an issue with the Web Vitals integration where LCP and CLS were not being captured correctly, +increasing capture rate by 10-30% for some apps. LCP and CLS capturing issues were introduced with version `7.75.0`. + +- fix(tracing): Ensure web vitals are correctly stopped/captured (#10323) + +### Other Changes + +- feat(react): Add `stripBasename` option for React Router 6. (#10314) +- fix(node): Fix `node-cron` types and add test (#10315) +- fix(node): Fix downleveled types entry point (#10321) +- fix(node): LocalVariables integration should use setupOnce (#10307) +- fix(replay): Fix type for options of replayIntegration (#10325) + +Work in this release contributed by @Shubhdeep12. Thank you for your contribution! + ## 7.95.0 ### Important Changes diff --git a/MIGRATION.md b/MIGRATION.md index 0253907e3278..4c0ea3eddc91 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -34,17 +34,31 @@ integrations from the `Integrations.XXX` hash, is deprecated in favor of using t The following list shows how integrations should be migrated: -| Old | New | Packages | -| ------------------------ | ------------------------------- | ------------------------------------------------------------------------------------------------------- | -| `new InboundFilters()` | `inboundFiltersIntegration()` | `@sentry/core`, `@sentry/browser`, `@sentry/node`, `@sentry/deno`, `@sentry/bun`, `@sentry/vercel-edge` | -| `new FunctionToString()` | `functionToStringIntegration()` | `@sentry/core`, `@sentry/browser`, `@sentry/node`, `@sentry/deno`, `@sentry/bun`, `@sentry/vercel-edge` | -| `new LinkedErrors()` | `linkedErrorsIntegration()` | `@sentry/core`, `@sentry/browser`, `@sentry/node`, `@sentry/deno`, `@sentry/bun`, `@sentry/vercel-edge` | -| `new ModuleMetadata()` | `moduleMetadataIntegration()` | `@sentry/core`, `@sentry/browser` | -| `new RequestData()` | `requestDataIntegration()` | `@sentry/core`, `@sentry/node`, `@sentry/deno`, `@sentry/bun`, `@sentry/vercel-edge` | -| `new Wasm() ` | `wasmIntegration()` | `@sentry/wasm` | -| `new Replay()` | `replayIntegration()` | `@sentry/browser` | -| `new ReplayCanvas()` | `replayCanvasIntegration()` | `@sentry/browser` | -| `new Feedback()` | `feedbackIntegration()` | `@sentry/browser` | +| Old | New | Packages | +| ------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------- | +| `new InboundFilters()` | `inboundFiltersIntegration()` | `@sentry/core`, `@sentry/browser`, `@sentry/node`, `@sentry/deno`, `@sentry/bun`, `@sentry/vercel-edge` | +| `new FunctionToString()` | `functionToStringIntegration()` | `@sentry/core`, `@sentry/browser`, `@sentry/node`, `@sentry/deno`, `@sentry/bun`, `@sentry/vercel-edge` | +| `new LinkedErrors()` | `linkedErrorsIntegration()` | `@sentry/core`, `@sentry/browser`, `@sentry/node`, `@sentry/deno`, `@sentry/bun`, `@sentry/vercel-edge` | +| `new ModuleMetadata()` | `moduleMetadataIntegration()` | `@sentry/core`, `@sentry/browser` | +| `new RequestData()` | `requestDataIntegration()` | `@sentry/core`, `@sentry/node`, `@sentry/deno`, `@sentry/bun`, `@sentry/vercel-edge` | +| `new Wasm() ` | `wasmIntegration()` | `@sentry/wasm` | +| `new Replay()` | `replayIntegration()` | `@sentry/browser` | +| `new ReplayCanvas()` | `replayCanvasIntegration()` | `@sentry/browser` | +| `new Feedback()` | `feedbackIntegration()` | `@sentry/browser` | +| `new CaptureConsole()` | `captureConsoleIntegration()` | `@sentry/integrations` | +| `new Debug()` | `debugIntegration()` | `@sentry/integrations` | +| `new Dedupe()` | `dedupeIntegration()` | `@sentry/browser`, `@sentry/integrations`, `@sentry/deno` | +| `new ExtraErrorData()` | `extraErrorDataIntegration()` | `@sentry/integrations` | +| `new ReportingObserver()` | `reportingObserverIntegration()` | `@sentry/integrations` | +| `new RewriteFrames()` | `rewriteFramesIntegration()` | `@sentry/integrations` | +| `new SessionTiming()` | `sessionTimingIntegration()` | `@sentry/integrations` | +| `new HttpClient()` | `httpClientIntegration()` | `@sentry/integrations` | +| `new ContextLines()` | `contextLinesIntegration()` | `@sentry/browser` | +| `new Breadcrumbs()` | `breadcrumbsIntegration()` | `@sentry/browser`, `@sentry/deno` | +| `new GlobalHandlers()` | `globalHandlersIntegration()` | `@sentry/browser` | +| `new HttpContext()` | `httpContextIntegration()` | `@sentry/browser` | +| `new TryCatch()` | `browserApiErrorsIntegration()` | `@sentry/browser`, `@sentry/deno` | +| `new VueIntegration()` | `vueIntegration()` | `@sentry/vue` | ## Deprecate `hub.bindClient()` and `makeMain()` diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/assets/sentry-logo-600x179.png b/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/assets/sentry-logo-600x179.png new file mode 100644 index 000000000000..353b7233d6bf Binary files /dev/null and b/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/assets/sentry-logo-600x179.png differ diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/subject.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/subject.js new file mode 100644 index 000000000000..d0f8df871ee3 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/subject.js @@ -0,0 +1,18 @@ +import { addLcpInstrumentationHandler } from '@sentry-internal/tracing'; + +addLcpInstrumentationHandler(({ metric }) => { + const entry = metric.entries[metric.entries.length - 1]; + window._LCP = entry.size; +}); + +addLcpInstrumentationHandler(({ metric }) => { + const entry = metric.entries[metric.entries.length - 1]; + window._LCP2 = entry.size; +}); + +window.ADD_HANDLER = () => { + addLcpInstrumentationHandler(({ metric }) => { + const entry = metric.entries[metric.entries.length - 1]; + window._LCP3 = entry.size; + }); +}; diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/template.html new file mode 100644 index 000000000000..caf4b8f2deab --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/template.html @@ -0,0 +1,11 @@ + + + + + + +
+ + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/test.ts new file mode 100644 index 000000000000..23cd29099a0f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/test.ts @@ -0,0 +1,51 @@ +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +const bundle = process.env.PW_BUNDLE || ''; + +sentryTest( + 'should capture metrics for LCP instrumentation handlers', + async ({ browserName, getLocalTestPath, page }) => { + // This uses a utility that is not exported in CDN bundles + if (shouldSkipTracingTest() || browserName !== 'chromium' || bundle.startsWith('bundle')) { + sentryTest.skip(); + } + + await page.route('**/path/to/image.png', (route: Route) => + route.fulfill({ path: `${__dirname}/assets/sentry-logo-600x179.png` }), + ); + + const url = await getLocalTestPath({ testDir: __dirname }); + const [eventData] = await Promise.all([ + getFirstSentryEnvelopeRequest(page), + page.goto(url), + page.click('button'), + ]); + + expect(eventData.measurements).toBeDefined(); + expect(eventData.measurements?.lcp?.value).toBeDefined(); + + expect(eventData.tags?.['lcp.element']).toBe('body > img'); + expect(eventData.tags?.['lcp.size']).toBe(107400); + expect(eventData.tags?.['lcp.url']).toBe('https://example.com/path/to/image.png'); + + const lcp = await (await page.waitForFunction('window._LCP')).jsonValue(); + const lcp2 = await (await page.waitForFunction('window._LCP2')).jsonValue(); + const lcp3 = await page.evaluate('window._LCP3'); + + expect(lcp).toEqual(107400); + expect(lcp2).toEqual(107400); + // this has not been triggered yet + expect(lcp3).toEqual(undefined); + + // Adding a handler after LCP is completed still triggers the handler + await page.evaluate('window.ADD_HANDLER()'); + const lcp3_2 = await (await page.waitForFunction('window._LCP3')).jsonValue(); + + expect(lcp3_2).toEqual(107400); + }, +); diff --git a/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/fixtures/ReplayRecordingData.ts b/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/fixtures/ReplayRecordingData.ts index 698cb83b5fb4..0b454ba12214 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/fixtures/ReplayRecordingData.ts +++ b/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/fixtures/ReplayRecordingData.ts @@ -215,7 +215,11 @@ export const ReplayRecordingData = [ description: 'largest-contentful-paint', startTimestamp: expect.any(Number), endTimestamp: expect.any(Number), - data: { value: expect.any(Number), size: expect.any(Number) }, + data: { + value: expect.any(Number), + size: expect.any(Number), + nodeId: 16, + }, }, }, }, diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tests/fixtures/ReplayRecordingData.ts b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tests/fixtures/ReplayRecordingData.ts index 698cb83b5fb4..0b454ba12214 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tests/fixtures/ReplayRecordingData.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tests/fixtures/ReplayRecordingData.ts @@ -215,7 +215,11 @@ export const ReplayRecordingData = [ description: 'largest-contentful-paint', startTimestamp: expect.any(Number), endTimestamp: expect.any(Number), - data: { value: expect.any(Number), size: expect.any(Number) }, + data: { + value: expect.any(Number), + size: expect.any(Number), + nodeId: 16, + }, }, }, }, diff --git a/dev-packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts b/dev-packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts index 698cb83b5fb4..0b454ba12214 100644 --- a/dev-packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts +++ b/dev-packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts @@ -215,7 +215,11 @@ export const ReplayRecordingData = [ description: 'largest-contentful-paint', startTimestamp: expect.any(Number), endTimestamp: expect.any(Number), - data: { value: expect.any(Number), size: expect.any(Number) }, + data: { + value: expect.any(Number), + size: expect.any(Number), + nodeId: 16, + }, }, }, }, diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index f36333c4daa8..10596903e2b2 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -36,10 +36,12 @@ "apollo-server": "^3.11.1", "axios": "^0.27.2", "cors": "^2.8.5", + "cron": "^3.1.6", "express": "^4.17.3", "graphql": "^16.3.0", "http-terminator": "^3.2.0", "mongodb": "^3.7.3", + "mongoose": "^5.13.22", "mongodb-memory-server-global": "^7.6.3", "mysql": "^2.18.1", "nock": "^13.1.0", diff --git a/dev-packages/node-integration-tests/suites/cron/node-cron/scenario.ts b/dev-packages/node-integration-tests/suites/cron/node-cron/scenario.ts new file mode 100644 index 000000000000..71b005d4dfd6 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/cron/node-cron/scenario.ts @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/node'; +import { CronJob } from 'cron'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const CronJobWithCheckIn = Sentry.cron.instrumentCron(CronJob, 'my-cron-job'); + +setTimeout(() => { + process.exit(0); +}, 1_000); diff --git a/dev-packages/node-integration-tests/suites/cron/node-cron/test.ts b/dev-packages/node-integration-tests/suites/cron/node-cron/test.ts new file mode 100644 index 000000000000..b768599cd215 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/cron/node-cron/test.ts @@ -0,0 +1,9 @@ +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('node-cron types should match', done => { + createRunner(__dirname, 'scenario.ts').ensureNoErrorOutput().start(done); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/scenario.js b/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/scenario.js new file mode 100644 index 000000000000..42bb8edacb8f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/scenario.js @@ -0,0 +1,52 @@ +const Sentry = require('@sentry/node-experimental'); +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +// Stop the process from exiting before the transaction is sent +setInterval(() => {}, 1000); + +async function run() { + const { ApolloServer, gql } = require('apollo-server'); + + await Sentry.startSpan( + { + name: 'Test Transaction', + op: 'transaction', + }, + async span => { + const typeDefs = gql`type Query { hello: String }`; + + const resolvers = { + Query: { + hello: () => { + return 'Hello world!'; + }, + }, + }; + + const server = new ApolloServer({ + typeDefs, + resolvers, + }); + + // Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation + await server.executeOperation({ + query: '{hello}', + }); + + setTimeout(() => { + span.end(); + server.stop(); + }, 500); + }, + ); +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/test.ts b/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/test.ts new file mode 100644 index 000000000000..dc7c304484f9 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/test.ts @@ -0,0 +1,38 @@ +import { conditionalTest } from '../../../utils'; +import { createRunner } from '../../../utils/runner'; + +conditionalTest({ min: 14 })('GraphQL/Apollo Tests', () => { + const EXPECTED_TRANSACTION = { + transaction: 'Test Transaction', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: { + 'graphql.operation.type': 'query', + 'graphql.source': '{hello}', + 'otel.kind': 'INTERNAL', + 'sentry.origin': 'auto.graphql.otel.graphql', + }, + description: 'query', + status: 'ok', + origin: 'auto.graphql.otel.graphql', + }), + expect.objectContaining({ + data: { + 'graphql.field.name': 'hello', + 'graphql.field.path': 'hello', + 'graphql.field.type': 'String', + 'graphql.source': 'hello', + 'otel.kind': 'INTERNAL', + 'sentry.origin': 'manual', + }, + description: 'graphql.resolve', + status: 'ok', + origin: 'manual', + }), + ]), + }; + + test('CJS - should instrument GraphQL queries used from Apollo Server.', done => { + createRunner(__dirname, 'scenario.js').expect({ transaction: EXPECTED_TRANSACTION }).start(done); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mongoose/scenario.js b/dev-packages/node-integration-tests/suites/tracing-experimental/mongoose/scenario.js new file mode 100644 index 000000000000..47a67e4aaf78 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing-experimental/mongoose/scenario.js @@ -0,0 +1,50 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node-experimental'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + debug: true, + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +// Stop the process from exiting before the transaction is sent +setInterval(() => {}, 1000); + +// Must be required after Sentry is initialized +const mongoose = require('mongoose'); + +async function run() { + await mongoose.connect(process.env.MONGO_URL || ''); + + const Schema = mongoose.Schema; + + const BlogPostSchema = new Schema({ + title: String, + body: String, + date: Date, + }); + + const BlogPost = mongoose.model('BlogPost', BlogPostSchema); + + await Sentry.startSpan( + { + name: 'Test Transaction', + op: 'transaction', + }, + async () => { + const post = new BlogPost(); + post.title = 'Test'; + post.body = 'Test body'; + post.date = new Date(); + + await post.save(); + + await BlogPost.findOne({}); + }, + ); +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mongoose/test.ts b/dev-packages/node-integration-tests/suites/tracing-experimental/mongoose/test.ts new file mode 100644 index 000000000000..1a246d8ec5b9 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing-experimental/mongoose/test.ts @@ -0,0 +1,54 @@ +import { MongoMemoryServer } from 'mongodb-memory-server-global'; + +import { conditionalTest } from '../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +jest.setTimeout(20000); + +conditionalTest({ min: 14 })('Mongoose experimental Test', () => { + let mongoServer: MongoMemoryServer; + + beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + process.env.MONGO_URL = mongoServer.getUri(); + }, 10000); + + afterAll(async () => { + if (mongoServer) { + await mongoServer.stop(); + } + cleanupChildProcesses(); + }); + + const EXPECTED_TRANSACTION = { + transaction: 'Test Transaction', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + 'db.mongodb.collection': 'blogposts', + 'db.name': 'test', + 'db.operation': 'save', + 'db.system': 'mongoose', + }), + description: 'mongoose.BlogPost.save', + op: 'db', + origin: 'auto.db.otel.mongoose', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.mongodb.collection': 'blogposts', + 'db.name': 'test', + 'db.operation': 'findOne', + 'db.system': 'mongoose', + }), + description: 'mongoose.BlogPost.findOne', + op: 'db', + origin: 'auto.db.otel.mongoose', + }), + ]), + }; + + test('CJS - should auto-instrument `mongoose` package.', done => { + createRunner(__dirname, 'scenario.js').expect({ transaction: EXPECTED_TRANSACTION }).start(done); + }); +}); diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts index 1831761c3181..31969452ba74 100644 --- a/dev-packages/node-integration-tests/utils/runner.ts +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -70,6 +70,7 @@ export function createRunner(...paths: string[]) { const flags: string[] = []; const ignored: EnvelopeItemType[] = []; let withSentryServer = false; + let ensureNoErrorOutput = false; if (testPath.endsWith('.ts')) { flags.push('-r', 'ts-node/register'); @@ -92,6 +93,10 @@ export function createRunner(...paths: string[]) { ignored.push(...types); return this; }, + ensureNoErrorOutput: function () { + ensureNoErrorOutput = true; + return this; + }, start: function (done?: (e?: unknown) => void) { const expectedEnvelopeCount = expectedEnvelopes.length; @@ -190,8 +195,19 @@ export function createRunner(...paths: string[]) { CHILD_PROCESSES.add(child); + if (ensureNoErrorOutput) { + child.stderr.on('data', (data: Buffer) => { + const output = data.toString(); + complete(new Error(`Expected no error output but got: '${output}'`)); + }); + } + child.on('close', () => { hasExited = true; + + if (ensureNoErrorOutput) { + complete(); + } }); // Pass error to done to end the test quickly diff --git a/packages/astro/src/index.types.ts b/packages/astro/src/index.types.ts index 146d6a6e23b0..026321e8ab3d 100644 --- a/packages/astro/src/index.types.ts +++ b/packages/astro/src/index.types.ts @@ -14,8 +14,11 @@ import sentryAstro from './index.server'; export declare function init(options: Options | clientSdk.BrowserOptions | serverSdk.NodeOptions): void; // We export a merged Integrations object so that users can (at least typing-wise) use all integrations everywhere. +// eslint-disable-next-line deprecation/deprecation export declare const Integrations: typeof clientSdk.Integrations & typeof serverSdk.Integrations; +export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; + export declare const defaultIntegrations: Integration[]; export declare const getDefaultIntegrations: (options: Options) => Integration[]; export declare const defaultStackParser: StackParser; diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index 5d012bdea2b7..e9e947de8559 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -102,4 +102,13 @@ export { // eslint-disable-next-line deprecation/deprecation wrap, } from './sdk'; + +export { breadcrumbsIntegration } from './integrations/breadcrumbs'; +export { dedupeIntegration } from './integrations/dedupe'; +export { globalHandlersIntegration } from './integrations/globalhandlers'; +export { httpContextIntegration } from './integrations/httpcontext'; +export { linkedErrorsIntegration } from './integrations/linkederrors'; +export { browserApiErrorsIntegration } from './integrations/trycatch'; + +// eslint-disable-next-line deprecation/deprecation export { GlobalHandlers, TryCatch, Breadcrumbs, LinkedErrors, HttpContext, Dedupe } from './integrations'; diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 6c91e141149a..19c377fc5931 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -12,6 +12,7 @@ if (WINDOW.Sentry && WINDOW.Sentry.Integrations) { windowIntegrations = WINDOW.Sentry.Integrations; } +/** @deprecated Import the integration function directly, e.g. `inboundFiltersIntegration()` instead of `new Integrations.InboundFilter(). */ const INTEGRATIONS = { ...windowIntegrations, // eslint-disable-next-line deprecation/deprecation @@ -19,6 +20,7 @@ const INTEGRATIONS = { ...BrowserIntegrations, }; +// eslint-disable-next-line deprecation/deprecation export { INTEGRATIONS as Integrations }; export { diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index 485b19d3b711..1cbd8321346e 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -1,5 +1,5 @@ /* eslint-disable max-lines */ -import { addBreadcrumb, convertIntegrationFnToClass, getClient } from '@sentry/core'; +import { addBreadcrumb, convertIntegrationFnToClass, defineIntegration, getClient } from '@sentry/core'; import type { Client, Event as SentryEvent, @@ -57,7 +57,7 @@ const MAX_ALLOWED_STRING_LENGTH = 1024; const INTEGRATION_NAME = 'Breadcrumbs'; -const breadcrumbsIntegration = ((options: Partial = {}) => { +const _breadcrumbsIntegration = ((options: Partial = {}) => { const _options = { console: true, dom: true, @@ -95,8 +95,12 @@ const breadcrumbsIntegration = ((options: Partial = {}) => { }; }) satisfies IntegrationFn; +export const breadcrumbsIntegration = defineIntegration(_breadcrumbsIntegration); + /** * Default Breadcrumbs instrumentations + * + * @deprecated Use `breadcrumbsIntegration()` instead. */ // eslint-disable-next-line deprecation/deprecation export const Breadcrumbs = convertIntegrationFnToClass(INTEGRATION_NAME, breadcrumbsIntegration) as IntegrationClass< diff --git a/packages/browser/src/integrations/dedupe.ts b/packages/browser/src/integrations/dedupe.ts index 394d5c5ae1e3..f4ace6011b19 100644 --- a/packages/browser/src/integrations/dedupe.ts +++ b/packages/browser/src/integrations/dedupe.ts @@ -1,4 +1,4 @@ -import { convertIntegrationFnToClass } from '@sentry/core'; +import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; import type { Event, Exception, Integration, IntegrationClass, IntegrationFn, StackFrame } from '@sentry/types'; import { logger } from '@sentry/utils'; @@ -6,7 +6,7 @@ import { DEBUG_BUILD } from '../debug-build'; const INTEGRATION_NAME = 'Dedupe'; -const dedupeIntegration = (() => { +const _dedupeIntegration = (() => { let previousEvent: Event | undefined; return { @@ -33,7 +33,12 @@ const dedupeIntegration = (() => { }; }) satisfies IntegrationFn; -/** Deduplication filter */ +export const dedupeIntegration = defineIntegration(_dedupeIntegration); + +/** + * Deduplication filter. + * @deprecated Use `dedupeIntegration()` instead. + */ // eslint-disable-next-line deprecation/deprecation export const Dedupe = convertIntegrationFnToClass(INTEGRATION_NAME, dedupeIntegration) as IntegrationClass< Integration & { processEvent: (event: Event) => Event } diff --git a/packages/browser/src/integrations/globalhandlers.ts b/packages/browser/src/integrations/globalhandlers.ts index 01ef16a02851..c1096e7c2132 100644 --- a/packages/browser/src/integrations/globalhandlers.ts +++ b/packages/browser/src/integrations/globalhandlers.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -import { captureEvent, convertIntegrationFnToClass, getClient } from '@sentry/core'; +import { captureEvent, convertIntegrationFnToClass, defineIntegration, getClient } from '@sentry/core'; import type { Client, Event, @@ -30,7 +30,7 @@ type GlobalHandlersIntegrations = Record = {}) => { +const _globalHandlersIntegration = ((options: Partial = {}) => { const _options = { onerror: true, onunhandledrejection: true, @@ -55,7 +55,12 @@ const globalHandlersIntegration = ((options: Partial }; }) satisfies IntegrationFn; -/** Global handlers */ +export const globalHandlersIntegration = defineIntegration(_globalHandlersIntegration); + +/** + * Global handlers. + * @deprecated Use `globalHandlersIntegration()` instead. + */ // eslint-disable-next-line deprecation/deprecation export const GlobalHandlers = convertIntegrationFnToClass( INTEGRATION_NAME, diff --git a/packages/browser/src/integrations/httpcontext.ts b/packages/browser/src/integrations/httpcontext.ts index 569eeffab45c..1a5e95f2eee3 100644 --- a/packages/browser/src/integrations/httpcontext.ts +++ b/packages/browser/src/integrations/httpcontext.ts @@ -1,11 +1,11 @@ -import { convertIntegrationFnToClass } from '@sentry/core'; +import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; import type { Event, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; import { WINDOW } from '../helpers'; const INTEGRATION_NAME = 'HttpContext'; -const httpContextIntegration = (() => { +const _httpContextIntegration = (() => { return { name: INTEGRATION_NAME, // TODO v8: Remove this @@ -33,7 +33,12 @@ const httpContextIntegration = (() => { }; }) satisfies IntegrationFn; -/** HttpContext integration collects information about HTTP request headers */ +export const httpContextIntegration = defineIntegration(_httpContextIntegration); + +/** + * HttpContext integration collects information about HTTP request headers. + * @deprecated Use `httpContextIntegration()` instead. + */ // eslint-disable-next-line deprecation/deprecation export const HttpContext = convertIntegrationFnToClass(INTEGRATION_NAME, httpContextIntegration) as IntegrationClass< Integration & { preprocessEvent: (event: Event) => void } diff --git a/packages/browser/src/integrations/index.ts b/packages/browser/src/integrations/index.ts index e029422f363c..17ac85b31232 100644 --- a/packages/browser/src/integrations/index.ts +++ b/packages/browser/src/integrations/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable deprecation/deprecation */ export { GlobalHandlers } from './globalhandlers'; export { TryCatch } from './trycatch'; export { Breadcrumbs } from './breadcrumbs'; diff --git a/packages/browser/src/integrations/linkederrors.ts b/packages/browser/src/integrations/linkederrors.ts index 8a166e7667d9..b03855303c58 100644 --- a/packages/browser/src/integrations/linkederrors.ts +++ b/packages/browser/src/integrations/linkederrors.ts @@ -1,4 +1,4 @@ -import { convertIntegrationFnToClass } from '@sentry/core'; +import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; import type { Client, Event, EventHint, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; import { applyAggregateErrorsToEvent } from '@sentry/utils'; import { exceptionFromError } from '../eventbuilder'; @@ -13,7 +13,7 @@ const DEFAULT_LIMIT = 5; const INTEGRATION_NAME = 'LinkedErrors'; -const linkedErrorsIntegration = ((options: LinkedErrorsOptions = {}) => { +const _linkedErrorsIntegration = ((options: LinkedErrorsOptions = {}) => { const limit = options.limit || DEFAULT_LIMIT; const key = options.key || DEFAULT_KEY; @@ -38,7 +38,12 @@ const linkedErrorsIntegration = ((options: LinkedErrorsOptions = {}) => { }; }) satisfies IntegrationFn; -/** Aggregrate linked errors in an event. */ +export const linkedErrorsIntegration = defineIntegration(_linkedErrorsIntegration); + +/** + * Aggregrate linked errors in an event. + * @deprecated Use `linkedErrorsIntegration()` instead. + */ // eslint-disable-next-line deprecation/deprecation export const LinkedErrors = convertIntegrationFnToClass(INTEGRATION_NAME, linkedErrorsIntegration) as IntegrationClass< Integration & { preprocessEvent: (event: Event, hint: EventHint, client: Client) => void } diff --git a/packages/browser/src/integrations/trycatch.ts b/packages/browser/src/integrations/trycatch.ts index 2f5f06592805..bd7c50331022 100644 --- a/packages/browser/src/integrations/trycatch.ts +++ b/packages/browser/src/integrations/trycatch.ts @@ -1,4 +1,4 @@ -import { convertIntegrationFnToClass } from '@sentry/core'; +import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; import type { Integration, IntegrationClass, IntegrationFn, WrappedFunction } from '@sentry/types'; import { fill, getFunctionName, getOriginalFunction } from '@sentry/utils'; @@ -50,7 +50,7 @@ interface TryCatchOptions { eventTarget: boolean | string[]; } -const browserApiErrorsIntegration = ((options: Partial = {}) => { +const _browserApiErrorsIntegration = ((options: Partial = {}) => { const _options = { XMLHttpRequest: true, eventTarget: true, @@ -90,7 +90,12 @@ const browserApiErrorsIntegration = ((options: Partial = {}) => }; }) satisfies IntegrationFn; -/** Wrap timer functions and event targets to catch errors and provide better meta data */ +export const browserApiErrorsIntegration = defineIntegration(_browserApiErrorsIntegration); + +/** + * Wrap timer functions and event targets to catch errors and provide better meta data. + * @deprecated Use `browserApiErrorsIntegration()` instead. + */ // eslint-disable-next-line deprecation/deprecation export const TryCatch = convertIntegrationFnToClass( INTEGRATION_NAME, diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index 6332cdac9d4b..1c57f534867c 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -1,7 +1,6 @@ import type { Hub } from '@sentry/core'; +import { functionToStringIntegration, inboundFiltersIntegration } from '@sentry/core'; import { - FunctionToString, - InboundFilters, captureSession, getClient, getCurrentHub, @@ -23,22 +22,25 @@ import { BrowserClient } from './client'; import { DEBUG_BUILD } from './debug-build'; import type { ReportDialogOptions } from './helpers'; import { WINDOW, wrap as internalWrap } from './helpers'; -import { Breadcrumbs, Dedupe, GlobalHandlers, HttpContext, LinkedErrors, TryCatch } from './integrations'; +import { breadcrumbsIntegration } from './integrations/breadcrumbs'; +import { dedupeIntegration } from './integrations/dedupe'; +import { globalHandlersIntegration } from './integrations/globalhandlers'; +import { httpContextIntegration } from './integrations/httpcontext'; +import { linkedErrorsIntegration } from './integrations/linkederrors'; +import { browserApiErrorsIntegration } from './integrations/trycatch'; import { defaultStackParser } from './stack-parsers'; import { makeFetchTransport, makeXHRTransport } from './transports'; /** @deprecated Use `getDefaultIntegrations(options)` instead. */ export const defaultIntegrations = [ - /* eslint-disable deprecation/deprecation */ - new InboundFilters(), - new FunctionToString(), - /* eslint-enable deprecation/deprecation */ - new TryCatch(), - new Breadcrumbs(), - new GlobalHandlers(), - new LinkedErrors(), - new Dedupe(), - new HttpContext(), + inboundFiltersIntegration(), + functionToStringIntegration(), + browserApiErrorsIntegration(), + breadcrumbsIntegration(), + globalHandlersIntegration(), + linkedErrorsIntegration(), + dedupeIntegration(), + httpContextIntegration(), ]; /** Get the default integrations for the browser SDK. */ diff --git a/packages/browser/test/unit/integrations/breadcrumbs.test.ts b/packages/browser/test/unit/integrations/breadcrumbs.test.ts index c2aea2ee170f..28764c2cddfa 100644 --- a/packages/browser/test/unit/integrations/breadcrumbs.test.ts +++ b/packages/browser/test/unit/integrations/breadcrumbs.test.ts @@ -8,6 +8,7 @@ describe('Breadcrumbs', () => { const client = new BrowserClient({ ...getDefaultBrowserClientOptions(), dsn: 'https://username@domain/123', + // eslint-disable-next-line deprecation/deprecation integrations: [new Breadcrumbs()], }); diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index 7adfcae9a894..65d82a0e6779 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -94,6 +94,7 @@ export { init, } from './sdk'; +export { breadcrumbsIntegration, dedupeIntegration } from '@sentry/browser'; import { Integrations as CoreIntegrations } from '@sentry/core'; import * as DenoIntegrations from './integrations'; diff --git a/packages/deno/src/sdk.ts b/packages/deno/src/sdk.ts index 81834a8b0081..990eb8146039 100644 --- a/packages/deno/src/sdk.ts +++ b/packages/deno/src/sdk.ts @@ -1,6 +1,6 @@ -import { Breadcrumbs, Dedupe } from '@sentry/browser'; +import { breadcrumbsIntegration, dedupeIntegration } from '@sentry/browser'; import type { ServerRuntimeClientOptions } from '@sentry/core'; -import { FunctionToString, InboundFilters, LinkedErrors } from '@sentry/core'; +import { functionToStringIntegration, inboundFiltersIntegration, linkedErrorsIntegration } from '@sentry/core'; import { getIntegrationsToSetup, initAndBind } from '@sentry/core'; import type { Integration, Options, StackParser } from '@sentry/types'; import { createStackParser, nodeStackLineParser, stackParserFromStackParserOptions } from '@sentry/utils'; @@ -12,15 +12,13 @@ import type { DenoOptions } from './types'; /** @deprecated Use `getDefaultIntegrations(options)` instead. */ export const defaultIntegrations = [ - /* eslint-disable deprecation/deprecation */ // Common - new InboundFilters(), - new FunctionToString(), - new LinkedErrors(), - /* eslint-enable deprecation/deprecation */ + inboundFiltersIntegration(), + functionToStringIntegration(), + linkedErrorsIntegration(), // From Browser - new Dedupe(), - new Breadcrumbs({ + dedupeIntegration(), + breadcrumbsIntegration({ dom: false, history: false, xhr: false, diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index 14c788a44307..d1d5e1db7ff5 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -18,7 +18,9 @@ export * from '@sentry/react'; export { nextRouterInstrumentation } from './routing/nextRoutingInstrumentation'; export { captureUnderscoreErrorException } from '../common/_error'; +/** @deprecated Import the integration function directly, e.g. `inboundFiltersIntegration()` instead of `new Integrations.InboundFilter(). */ export const Integrations = { + // eslint-disable-next-line deprecation/deprecation ...OriginalIntegrations, BrowserTracing, }; diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts index b15bb6c40bab..21d51a01ee56 100644 --- a/packages/nextjs/src/index.types.ts +++ b/packages/nextjs/src/index.types.ts @@ -20,10 +20,13 @@ export declare function init( ): void; // We export a merged Integrations object so that users can (at least typing-wise) use all integrations everywhere. +// eslint-disable-next-line deprecation/deprecation export declare const Integrations: typeof clientSdk.Integrations & typeof serverSdk.Integrations & typeof edgeSdk.Integrations; +export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; + export declare const defaultIntegrations: Integration[]; export declare const getDefaultIntegrations: (options: Options) => Integration[]; export declare const defaultStackParser: StackParser; diff --git a/packages/nextjs/test/clientSdk.test.ts b/packages/nextjs/test/clientSdk.test.ts index d0f0de959170..464b7db14dc7 100644 --- a/packages/nextjs/test/clientSdk.test.ts +++ b/packages/nextjs/test/clientSdk.test.ts @@ -6,7 +6,7 @@ import type { Integration } from '@sentry/types'; import { logger } from '@sentry/utils'; import { JSDOM } from 'jsdom'; -import { BrowserTracing, Integrations, init, nextRouterInstrumentation } from '../src/client'; +import { BrowserTracing, breadcrumbsIntegration, init, nextRouterInstrumentation } from '../src/client'; const reactInit = jest.spyOn(SentryReact, 'init'); const captureEvent = jest.spyOn(BaseClient.prototype, 'captureEvent'); @@ -108,12 +108,12 @@ describe('Client init()', () => { type ModifiedInitOptionsIntegrationArray = { defaultIntegrations: Integration[]; integrations: Integration[] }; it('supports passing unrelated integrations through options', () => { - init({ integrations: [new Integrations.Breadcrumbs({ console: false })] }); + init({ integrations: [breadcrumbsIntegration({ console: false })] }); const reactInitOptions = reactInit.mock.calls[0][0] as ModifiedInitOptionsIntegrationArray; - const breadcrumbsIntegration = findIntegrationByName(reactInitOptions.integrations, 'Breadcrumbs'); + const installedBreadcrumbsIntegration = findIntegrationByName(reactInitOptions.integrations, 'Breadcrumbs'); - expect(breadcrumbsIntegration).toBeDefined(); + expect(installedBreadcrumbsIntegration).toBeDefined(); }); describe('`BrowserTracing` integration', () => { diff --git a/packages/node/package.json b/packages/node/package.json index b096befa6b39..eb8b3056b149 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -20,8 +20,8 @@ "types": "build/types/index.d.ts", "typesVersions": { "<4.9": { - "build/npm/types/index.d.ts": [ - "build/npm/types-ts3.8/index.d.ts" + "build/types/index.d.ts": [ + "build/types-ts3.8/index.d.ts" ] } }, diff --git a/packages/node/src/cron/cron.ts b/packages/node/src/cron/cron.ts index 88a3e9e58eb5..2540d82e736b 100644 --- a/packages/node/src/cron/cron.ts +++ b/packages/node/src/cron/cron.ts @@ -8,10 +8,17 @@ export type CronJobParams = { start?: boolean | null; context?: unknown; runOnInit?: boolean | null; - utcOffset?: number; - timeZone?: string; unrefTimeout?: boolean | null; -}; +} & ( + | { + timeZone?: string | null; + utcOffset?: never; + } + | { + timeZone?: never; + utcOffset?: number | null; + } +); export type CronJob = { // @@ -28,6 +35,17 @@ export type CronJobConstructor = { timeZone?: CronJobParams['timeZone'], context?: CronJobParams['context'], runOnInit?: CronJobParams['runOnInit'], + utcOffset?: null, + unrefTimeout?: CronJobParams['unrefTimeout'], + ): CronJob; + new ( + cronTime: CronJobParams['cronTime'], + onTick: CronJobParams['onTick'], + onComplete?: CronJobParams['onComplete'], + start?: CronJobParams['start'], + timeZone?: null, + context?: CronJobParams['context'], + runOnInit?: CronJobParams['runOnInit'], utcOffset?: CronJobParams['utcOffset'], unrefTimeout?: CronJobParams['unrefTimeout'], ): CronJob; 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 bfe255002975..93f3b61b10f2 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,5 @@ /* eslint-disable max-lines */ -import { convertIntegrationFnToClass } from '@sentry/core'; +import { convertIntegrationFnToClass, getClient } from '@sentry/core'; import type { Event, Exception, Integration, IntegrationClass, IntegrationFn, StackParser } from '@sentry/types'; import { LRUMap, logger } from '@sentry/utils'; import type { Debugger, InspectorNotification, Runtime, Session } from 'inspector'; @@ -326,12 +326,11 @@ const localVariablesSyncIntegration = (( return { name: INTEGRATION_NAME, - // TODO v8: Remove this - setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function - setup(client: NodeClient) { - const clientOptions = client.getOptions(); + setupOnce() { + const client = getClient(); + const clientOptions = client?.getOptions(); - if (session && clientOptions.includeLocalVariables) { + 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_VERSION.major < 18; diff --git a/packages/node/test/integrations/localvariables.test.ts b/packages/node/test/integrations/localvariables.test.ts index b0fc6094e6a5..abc1d241f842 100644 --- a/packages/node/test/integrations/localvariables.test.ts +++ b/packages/node/test/integrations/localvariables.test.ts @@ -1,356 +1,12 @@ -import type { Debugger, InspectorNotification } from 'inspector'; - -import { NodeClient, defaultStackParser } from '../../src'; import { createRateLimiter } from '../../src/integrations/local-variables/common'; -import type { FrameVariables } from '../../src/integrations/local-variables/common'; -import type { DebugSession } from '../../src/integrations/local-variables/local-variables-sync'; -import { LocalVariablesSync, createCallbackList } from '../../src/integrations/local-variables/local-variables-sync'; +import { createCallbackList } from '../../src/integrations/local-variables/local-variables-sync'; import { NODE_VERSION } from '../../src/nodeVersion'; -import { getDefaultNodeClientOptions } from '../../test/helper/node-client-options'; jest.setTimeout(20_000); const describeIf = (condition: boolean) => (condition ? describe : describe.skip); -interface ThrowOn { - configureAndConnect?: boolean; - getLocalVariables?: boolean; -} - -class MockDebugSession implements DebugSession { - private _onPause?: (message: InspectorNotification, callback: () => void) => void; - - constructor(private readonly _vars: Record>, private readonly _throwOn?: ThrowOn) {} - - public configureAndConnect( - onPause: (message: InspectorNotification, callback: () => void) => void, - _captureAll: boolean, - ): void { - if (this._throwOn?.configureAndConnect) { - throw new Error('configureAndConnect should not be called'); - } - - this._onPause = onPause; - } - - public setPauseOnExceptions(_: boolean): void {} - - public getLocalVariables(objectId: string, callback: (vars: Record) => void): void { - if (this._throwOn?.getLocalVariables) { - throw new Error('getLocalVariables should not be called'); - } - - callback(this._vars[objectId]); - } - - public runPause(message: InspectorNotification): Promise { - return new Promise(resolve => { - this._onPause?.(message, resolve); - }); - } -} - -interface LocalVariablesPrivate { - _getCachedFramesCount(): number; - _getFirstCachedFrame(): FrameVariables[] | undefined; -} - -const exceptionEvent = { - method: 'Debugger.paused', - params: { - reason: 'exception', - data: { - description: - 'Error: Some error\n' + - ' at two (/dist/javascript/src/main.js:23:9)\n' + - ' at one (/dist/javascript/src/main.js:19:3)\n' + - ' at Timeout._onTimeout (/dist/javascript/src/main.js:40:5)\n' + - ' at listOnTimeout (node:internal/timers:559:17)\n' + - ' at process.processTimers (node:internal/timers:502:7)', - }, - callFrames: [ - { - callFrameId: '-6224981551105448869.1.0', - functionName: 'two', - location: { scriptId: '134', lineNumber: 22 }, - url: '', - scopeChain: [ - { - type: 'local', - object: { - type: 'object', - className: 'Object', - objectId: '-6224981551105448869.1.2', - }, - name: 'two', - }, - ], - this: { - type: 'object', - className: 'global', - }, - }, - { - callFrameId: '-6224981551105448869.1.1', - functionName: 'one', - location: { scriptId: '134', lineNumber: 18 }, - url: '', - scopeChain: [ - { - type: 'local', - object: { - type: 'object', - className: 'Object', - objectId: '-6224981551105448869.1.6', - }, - name: 'one', - }, - ], - this: { - type: 'object', - className: 'global', - }, - }, - ], - }, -}; - -const exceptionEvent100Frames = { - method: 'Debugger.paused', - params: { - reason: 'exception', - data: { - description: - 'Error: Some error\n' + - ' at two (/dist/javascript/src/main.js:23:9)\n' + - ' at one (/dist/javascript/src/main.js:19:3)\n' + - ' at Timeout._onTimeout (/dist/javascript/src/main.js:40:5)\n' + - ' at listOnTimeout (node:internal/timers:559:17)\n' + - ' at process.processTimers (node:internal/timers:502:7)', - }, - callFrames: new Array(100).fill({ - callFrameId: '-6224981551105448869.1.0', - functionName: 'two', - location: { scriptId: '134', lineNumber: 22 }, - url: '', - scopeChain: [ - { - type: 'local', - object: { - type: 'object', - className: 'Object', - objectId: '-6224981551105448869.1.2', - }, - name: 'two', - }, - ], - this: { - type: 'object', - className: 'global', - }, - }), - }, -}; - describeIf(NODE_VERSION.major >= 18)('LocalVariables', () => { - it('Adds local variables to stack frames', async () => { - const session = new MockDebugSession({ - '-6224981551105448869.1.2': { name: 'tim' }, - '-6224981551105448869.1.6': { arr: [1, 2, 3] }, - }); - const localVariables = new LocalVariablesSync({}, session); - const options = getDefaultNodeClientOptions({ - stackParser: defaultStackParser, - includeLocalVariables: true, - integrations: [], - }); - - const client = new NodeClient(options); - client.addIntegration(localVariables); - - const eventProcessors = client['_eventProcessors']; - const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariables'); - - expect(eventProcessor).toBeDefined(); - - await session.runPause(exceptionEvent); - - expect((localVariables as unknown as LocalVariablesPrivate)._getCachedFramesCount()).toBe(1); - - const frames = (localVariables as unknown as LocalVariablesPrivate)._getFirstCachedFrame(); - - expect(frames).toBeDefined(); - - const vars = frames as FrameVariables[]; - - expect(vars).toEqual([ - { function: 'two', vars: { name: 'tim' } }, - { function: 'one', vars: { arr: [1, 2, 3] } }, - ]); - - const event = await eventProcessor!( - { - event_id: '9cbf882ade9a415986632ac4e16918eb', - platform: 'node', - timestamp: 1671113680.306, - level: 'fatal', - exception: { - values: [ - { - type: 'Error', - value: 'Some error', - stacktrace: { - frames: [ - { - function: 'process.processTimers', - lineno: 502, - colno: 7, - in_app: false, - }, - { - function: 'listOnTimeout', - lineno: 559, - colno: 17, - in_app: false, - }, - { - function: 'Timeout._onTimeout', - lineno: 40, - colno: 5, - in_app: true, - }, - { - function: 'one', - lineno: 19, - colno: 3, - in_app: true, - }, - { - function: 'two', - lineno: 23, - colno: 9, - in_app: true, - }, - ], - }, - mechanism: { type: 'generic', handled: true }, - }, - ], - }, - }, - {}, - ); - - expect(event?.exception?.values?.[0].stacktrace?.frames?.[3]?.vars).toEqual({ arr: [1, 2, 3] }); - expect(event?.exception?.values?.[0].stacktrace?.frames?.[4]?.vars).toEqual({ name: 'tim' }); - - expect((localVariables as unknown as LocalVariablesPrivate)._getCachedFramesCount()).toBe(0); - }); - - it('Only considers the first 5 frames', async () => { - const session = new MockDebugSession({}); - const localVariables = new LocalVariablesSync({}, session); - const options = getDefaultNodeClientOptions({ - stackParser: defaultStackParser, - includeLocalVariables: true, - integrations: [], - }); - - const client = new NodeClient(options); - client.addIntegration(localVariables); - - await session.runPause(exceptionEvent100Frames); - - expect((localVariables as unknown as LocalVariablesPrivate)._getCachedFramesCount()).toBe(1); - - const frames = (localVariables as unknown as LocalVariablesPrivate)._getFirstCachedFrame(); - - expect(frames).toBeDefined(); - - const vars = frames as FrameVariables[]; - - expect(vars.length).toEqual(5); - }); - - it('Should not lookup variables for non-exception reasons', async () => { - const session = new MockDebugSession({}, { getLocalVariables: true }); - const localVariables = new LocalVariablesSync({}, session); - const options = getDefaultNodeClientOptions({ - stackParser: defaultStackParser, - includeLocalVariables: true, - integrations: [], - }); - - const client = new NodeClient(options); - client.addIntegration(localVariables); - - const nonExceptionEvent = { - method: exceptionEvent.method, - params: { ...exceptionEvent.params, reason: 'non-exception-reason' }, - }; - - await session.runPause(nonExceptionEvent); - - expect((localVariables as unknown as LocalVariablesPrivate)._getCachedFramesCount()).toBe(0); - }); - - it('Should not initialize when disabled', async () => { - const session = new MockDebugSession({}, { configureAndConnect: true }); - const localVariables = new LocalVariablesSync({}, session); - const options = getDefaultNodeClientOptions({ - stackParser: defaultStackParser, - integrations: [], - }); - - const client = new NodeClient(options); - client.addIntegration(localVariables); - - const eventProcessors = client['_eventProcessors']; - const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariables'); - - expect(eventProcessor).toBeDefined(); - }); - - it('Should not initialize when inspector not loaded', async () => { - const localVariables = new LocalVariablesSync({}, undefined); - const options = getDefaultNodeClientOptions({ - stackParser: defaultStackParser, - integrations: [], - }); - - const client = new NodeClient(options); - client.addIntegration(localVariables); - - const eventProcessors = client['_eventProcessors']; - const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariables'); - - expect(eventProcessor).toBeDefined(); - }); - - it('Should cache identical uncaught exception events', async () => { - const session = new MockDebugSession({ - '-6224981551105448869.1.2': { name: 'tim' }, - '-6224981551105448869.1.6': { arr: [1, 2, 3] }, - }); - const localVariables = new LocalVariablesSync({}, session); - const options = getDefaultNodeClientOptions({ - stackParser: defaultStackParser, - includeLocalVariables: true, - integrations: [], - }); - - const client = new NodeClient(options); - client.addIntegration(localVariables); - - await session.runPause(exceptionEvent); - await session.runPause(exceptionEvent); - await session.runPause(exceptionEvent); - await session.runPause(exceptionEvent); - await session.runPause(exceptionEvent); - - expect((localVariables as unknown as LocalVariablesPrivate)._getCachedFramesCount()).toBe(1); - }); - describe('createCallbackList', () => { it('Should call callbacks in reverse order', done => { const log: number[] = []; diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index de87e5bb6881..c2dc56687571 100644 --- a/packages/react/src/reactrouterv6.tsx +++ b/packages/react/src/reactrouterv6.tsx @@ -35,6 +35,7 @@ let _createRoutesFromChildren: CreateRoutesFromChildren; let _matchRoutes: MatchRoutes; let _customStartTransaction: (context: TransactionContext) => Transaction | undefined; let _startTransactionOnLocationChange: boolean; +let _stripBasename: boolean = false; const SENTRY_TAGS = { 'routing.instrumentation': 'react-router-v6', @@ -46,6 +47,7 @@ export function reactRouterV6Instrumentation( useNavigationType: UseNavigationType, createRoutesFromChildren: CreateRoutesFromChildren, matchRoutes: MatchRoutes, + stripBasename?: boolean, ) { return ( customStartTransaction: (context: TransactionContext) => Transaction | undefined, @@ -70,12 +72,40 @@ export function reactRouterV6Instrumentation( _useNavigationType = useNavigationType; _matchRoutes = matchRoutes; _createRoutesFromChildren = createRoutesFromChildren; + _stripBasename = stripBasename || false; _customStartTransaction = customStartTransaction; _startTransactionOnLocationChange = startTransactionOnLocationChange; }; } +/** + * Strip the basename from a pathname if exists. + * + * Vendored and modified from `react-router` + * https://github.com/remix-run/react-router/blob/462bb712156a3f739d6139a0f14810b76b002df6/packages/router/utils.ts#L1038 + */ +function stripBasenameFromPathname(pathname: string, basename: string): string { + if (!basename || basename === '/') { + return pathname; + } + + if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) { + return pathname; + } + + // We want to leave trailing slash behavior in the user's control, so if they + // specify a basename with a trailing slash, we should support it + const startIndex = basename.endsWith('/') ? basename.length - 1 : basename.length; + const nextChar = pathname.charAt(startIndex); + if (nextChar && nextChar !== '/') { + // pathname does not start with basename/ + return pathname; + } + + return pathname.slice(startIndex) || '/'; +} + function getNormalizedName( routes: RouteObject[], location: Location, @@ -83,7 +113,7 @@ function getNormalizedName( basename: string = '', ): [string, TransactionSource] { if (!routes || routes.length === 0) { - return [location.pathname, 'url']; + return [_stripBasename ? stripBasenameFromPathname(location.pathname, basename) : location.pathname, 'url']; } let pathBuilder = ''; @@ -95,7 +125,7 @@ function getNormalizedName( if (route) { // Early return if index route if (route.index) { - return [branch.pathname, 'route']; + return [_stripBasename ? stripBasenameFromPathname(branch.pathname, basename) : branch.pathname, 'route']; } const path = route.path; @@ -112,16 +142,16 @@ function getNormalizedName( // We should not count wildcard operators in the url segments calculation pathBuilder.slice(-2) !== '/*' ) { - return [basename + newPath, 'route']; + return [(_stripBasename ? '' : basename) + newPath, 'route']; } - return [basename + pathBuilder, 'route']; + return [(_stripBasename ? '' : basename) + pathBuilder, 'route']; } } } } } - return [location.pathname, 'url']; + return [_stripBasename ? stripBasenameFromPathname(location.pathname, basename) : location.pathname, 'url']; } function updatePageloadTransaction( diff --git a/packages/react/test/reactrouterv6.4.test.tsx b/packages/react/test/reactrouterv6.4.test.tsx index d6b9c0c45b49..29fe612f7e97 100644 --- a/packages/react/test/reactrouterv6.4.test.tsx +++ b/packages/react/test/reactrouterv6.4.test.tsx @@ -26,6 +26,7 @@ describe('React Router v6.4', () => { function createInstrumentation(_opts?: { startTransactionOnPageLoad?: boolean; startTransactionOnLocationChange?: boolean; + stripBasename?: boolean; }): [jest.Mock, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetAttribute: jest.Mock }] { const options = { matchPath: _opts ? matchPath : undefined, @@ -46,6 +47,7 @@ describe('React Router v6.4', () => { useNavigationType, createRoutesFromChildren, matchRoutes, + options.stripBasename, )(mockStartTransaction, options.startTransactionOnPageLoad, options.startTransactionOnLocationChange); return [mockStartTransaction, { mockUpdateName, mockFinish, mockSetAttribute }]; } @@ -359,5 +361,93 @@ describe('React Router v6.4', () => { metadata: { source: 'route' }, }); }); + + it('strips `basename` from transaction names of parameterized paths', () => { + const [mockStartTransaction] = createInstrumentation({ + stripBasename: true, + }); + const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); + + const router = sentryCreateBrowserRouter( + [ + { + path: '/', + element: , + }, + { + path: ':orgId', + children: [ + { + path: 'users', + children: [ + { + path: ':userId', + element:
User
, + }, + ], + }, + ], + }, + ], + { + initialEntries: ['/admin'], + basename: '/admin', + }, + ); + + // @ts-expect-error router is fine + render(); + + expect(mockStartTransaction).toHaveBeenCalledTimes(2); + expect(mockStartTransaction).toHaveBeenLastCalledWith({ + name: '/:orgId/users/:userId', + op: 'navigation', + origin: 'auto.navigation.react.reactrouterv6', + tags: { 'routing.instrumentation': 'react-router-v6' }, + metadata: { source: 'route' }, + }); + }); + + it('strips `basename` from transaction names of non-parameterized paths', () => { + const [mockStartTransaction] = createInstrumentation({ + stripBasename: true, + }); + const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); + + const router = sentryCreateBrowserRouter( + [ + { + path: '/', + element: , + }, + { + path: 'about', + element:
About
, + children: [ + { + path: 'us', + element:
Us
, + }, + ], + }, + ], + { + initialEntries: ['/app'], + basename: '/app', + }, + ); + + // @ts-expect-error router is fine + render(); + + expect(mockStartTransaction).toHaveBeenCalledTimes(2); + expect(mockStartTransaction).toHaveBeenLastCalledWith({ + name: '/about/us', + op: 'navigation', + origin: 'auto.navigation.react.reactrouterv6', + tags: { 'routing.instrumentation': 'react-router-v6' }, + metadata: { source: 'route' }, + }); + }); }); }); diff --git a/packages/remix/src/index.types.ts b/packages/remix/src/index.types.ts index 1a7c7fb847e5..0abe77c7a20d 100644 --- a/packages/remix/src/index.types.ts +++ b/packages/remix/src/index.types.ts @@ -13,8 +13,11 @@ import type { RemixOptions } from './utils/remixOptions'; export declare function init(options: RemixOptions): void; // We export a merged Integrations object so that users can (at least typing-wise) use all integrations everywhere. +// eslint-disable-next-line deprecation/deprecation export declare const Integrations: typeof clientSdk.Integrations & typeof serverSdk.Integrations; +export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; + export declare const defaultIntegrations: Integration[]; export declare const getDefaultIntegrations: (options: Options) => Integration[]; export declare const defaultStackParser: StackParser; diff --git a/packages/replay/src/integration.ts b/packages/replay/src/integration.ts index 9f4d03e7f5fa..3434888f6574 100644 --- a/packages/replay/src/integration.ts +++ b/packages/replay/src/integration.ts @@ -30,7 +30,7 @@ let _initialized = false; type InitialReplayPluginOptions = Omit & Partial>; -export const replayIntegration = ((options?: InitialReplayPluginOptions) => { +export const replayIntegration = ((options?: ReplayConfiguration) => { // eslint-disable-next-line deprecation/deprecation return new Replay(options); }) satisfies IntegrationFn; diff --git a/packages/sveltekit/src/index.types.ts b/packages/sveltekit/src/index.types.ts index 3e57a08c2538..6a5d3e3883e9 100644 --- a/packages/sveltekit/src/index.types.ts +++ b/packages/sveltekit/src/index.types.ts @@ -37,8 +37,11 @@ export declare function handleErrorWithSentry any>(origLoad: T): T; // We export a merged Integrations object so that users can (at least typing-wise) use all integrations everywhere. +// eslint-disable-next-line deprecation/deprecation export declare const Integrations: typeof clientSdk.Integrations & typeof serverSdk.Integrations; +export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; + export declare const defaultIntegrations: Integration[]; export declare const getDefaultIntegrations: (options: Options) => Integration[]; export declare const defaultStackParser: StackParser; diff --git a/packages/tracing-internal/src/browser/instrument.ts b/packages/tracing-internal/src/browser/instrument.ts index ae4532259f94..2a4e7acaf3b1 100644 --- a/packages/tracing-internal/src/browser/instrument.ts +++ b/packages/tracing-internal/src/browser/instrument.ts @@ -73,6 +73,8 @@ interface Metric { type InstrumentHandlerType = InstrumentHandlerTypeMetric | InstrumentHandlerTypePerformanceObserver; +type StopListening = undefined | void | (() => void); + // eslint-disable-next-line @typescript-eslint/no-explicit-any type InstrumentHandlerCallback = (data: any) => void; @@ -88,17 +90,29 @@ let _previousLcp: Metric | undefined; /** * Add a callback that will be triggered when a CLS metric is available. * Returns a cleanup callback which can be called to remove the instrumentation handler. + * + * Pass `stopOnCallback = true` to stop listening for CLS when the cleanup callback is called. + * This will lead to the CLS being finalized and frozen. */ -export function addClsInstrumentationHandler(callback: (data: { metric: Metric }) => void): CleanupHandlerCallback { - return addMetricObserver('cls', callback, instrumentCls, _previousCls); +export function addClsInstrumentationHandler( + callback: (data: { metric: Metric }) => void, + stopOnCallback = false, +): CleanupHandlerCallback { + return addMetricObserver('cls', callback, instrumentCls, _previousCls, stopOnCallback); } /** * Add a callback that will be triggered when a LCP metric is available. * Returns a cleanup callback which can be called to remove the instrumentation handler. + * + * Pass `stopOnCallback = true` to stop listening for LCP when the cleanup callback is called. + * This will lead to the LCP being finalized and frozen. */ -export function addLcpInstrumentationHandler(callback: (data: { metric: Metric }) => void): CleanupHandlerCallback { - return addMetricObserver('lcp', callback, instrumentLcp, _previousLcp); +export function addLcpInstrumentationHandler( + callback: (data: { metric: Metric }) => void, + stopOnCallback = false, +): CleanupHandlerCallback { + return addMetricObserver('lcp', callback, instrumentLcp, _previousLcp, stopOnCallback); } /** @@ -158,8 +172,8 @@ function triggerHandlers(type: InstrumentHandlerType, data: unknown): void { } } -function instrumentCls(): void { - onCLS(metric => { +function instrumentCls(): StopListening { + return onCLS(metric => { triggerHandlers('cls', { metric, }); @@ -168,7 +182,7 @@ function instrumentCls(): void { } function instrumentFid(): void { - onFID(metric => { + return onFID(metric => { triggerHandlers('fid', { metric, }); @@ -176,8 +190,8 @@ function instrumentFid(): void { }); } -function instrumentLcp(): void { - onLCP(metric => { +function instrumentLcp(): StopListening { + return onLCP(metric => { triggerHandlers('lcp', { metric, }); @@ -188,13 +202,16 @@ function instrumentLcp(): void { function addMetricObserver( type: InstrumentHandlerTypeMetric, callback: InstrumentHandlerCallback, - instrumentFn: () => void, + instrumentFn: () => StopListening, previousValue: Metric | undefined, + stopOnCallback = false, ): CleanupHandlerCallback { addHandler(type, callback); + let stopListening: StopListening | undefined; + if (!instrumented[type]) { - instrumentFn(); + stopListening = instrumentFn(); instrumented[type] = true; } @@ -202,7 +219,7 @@ function addMetricObserver( callback({ metric: previousValue }); } - return getCleanupCallback(type, callback); + return getCleanupCallback(type, callback, stopOnCallback ? stopListening : undefined); } function instrumentPerformanceObserver(type: InstrumentHandlerTypePerformanceObserver): void { @@ -228,8 +245,16 @@ function addHandler(type: InstrumentHandlerType, handler: InstrumentHandlerCallb } // Get a callback which can be called to remove the instrumentation handler -function getCleanupCallback(type: InstrumentHandlerType, callback: InstrumentHandlerCallback): CleanupHandlerCallback { +function getCleanupCallback( + type: InstrumentHandlerType, + callback: InstrumentHandlerCallback, + stopListening: StopListening, +): CleanupHandlerCallback { return () => { + if (stopListening) { + stopListening(); + } + const typeHandlers = handlers[type]; if (!typeHandlers) { diff --git a/packages/tracing-internal/src/browser/metrics/index.ts b/packages/tracing-internal/src/browser/metrics/index.ts index d0eb73b945d2..4c9c25111e11 100644 --- a/packages/tracing-internal/src/browser/metrics/index.ts +++ b/packages/tracing-internal/src/browser/metrics/index.ts @@ -39,7 +39,8 @@ let _lcpEntry: LargestContentfulPaint | undefined; let _clsEntry: LayoutShift | undefined; /** - * Start tracking web vitals + * Start tracking web vitals. + * The callback returned by this function can be used to stop tracking & ensure all measurements are final & captured. * * @returns A function that forces web vitals collection */ @@ -129,7 +130,7 @@ export function startTrackingInteractions(): void { /** Starts tracking the Cumulative Layout Shift on the current page. */ function _trackCLS(): () => void { return addClsInstrumentationHandler(({ metric }) => { - const entry = metric.entries.pop(); + const entry = metric.entries[metric.entries.length - 1]; if (!entry) { return; } @@ -137,13 +138,13 @@ function _trackCLS(): () => void { DEBUG_BUILD && logger.log('[Measurements] Adding CLS'); _measurements['cls'] = { value: metric.value, unit: '' }; _clsEntry = entry as LayoutShift; - }); + }, true); } /** Starts tracking the Largest Contentful Paint on the current page. */ function _trackLCP(): () => void { return addLcpInstrumentationHandler(({ metric }) => { - const entry = metric.entries.pop(); + const entry = metric.entries[metric.entries.length - 1]; if (!entry) { return; } @@ -151,13 +152,13 @@ function _trackLCP(): () => void { DEBUG_BUILD && logger.log('[Measurements] Adding LCP'); _measurements['lcp'] = { value: metric.value, unit: 'millisecond' }; _lcpEntry = entry as LargestContentfulPaint; - }); + }, true); } /** Starts tracking the First Input Delay on the current page. */ function _trackFID(): () => void { return addFidInstrumentationHandler(({ metric }) => { - const entry = metric.entries.pop(); + const entry = metric.entries[metric.entries.length - 1]; if (!entry) { return; } diff --git a/packages/tracing-internal/src/browser/request.ts b/packages/tracing-internal/src/browser/request.ts index 461171ec1e25..c84d0545054b 100644 --- a/packages/tracing-internal/src/browser/request.ts +++ b/packages/tracing-internal/src/browser/request.ts @@ -1,6 +1,5 @@ /* eslint-disable max-lines */ import { - getActiveSpan, getClient, getCurrentScope, getDynamicSamplingContextFromClient, @@ -9,6 +8,7 @@ import { hasTracingEnabled, spanToJSON, spanToTraceHeader, + startInactiveSpan, } from '@sentry/core'; import type { HandlerDataXhr, SentryWrappedXMLHttpRequest, Span } from '@sentry/types'; import { @@ -275,22 +275,19 @@ export function xhrCallback( } const scope = getCurrentScope(); - const parentSpan = getActiveSpan(); - - const span = - shouldCreateSpanResult && parentSpan - ? // eslint-disable-next-line deprecation/deprecation - parentSpan.startChild({ - data: { - type: 'xhr', - 'http.method': sentryXhrData.method, - url: sentryXhrData.url, - }, - description: `${sentryXhrData.method} ${sentryXhrData.url}`, - op: 'http.client', - origin: 'auto.http.browser', - }) - : undefined; + + const span = shouldCreateSpanResult + ? startInactiveSpan({ + attributes: { + type: 'xhr', + 'http.method': sentryXhrData.method, + url: sentryXhrData.url, + }, + name: `${sentryXhrData.method} ${sentryXhrData.url}`, + op: 'http.client', + origin: 'auto.http.browser', + }) + : undefined; if (span) { xhr.__sentry_xhr_span_id__ = span.spanContext().spanId; diff --git a/packages/tracing-internal/src/common/fetch.ts b/packages/tracing-internal/src/common/fetch.ts index e0a8e2ed9fa3..c96778f8cd35 100644 --- a/packages/tracing-internal/src/common/fetch.ts +++ b/packages/tracing-internal/src/common/fetch.ts @@ -1,5 +1,4 @@ import { - getActiveSpan, getClient, getCurrentScope, getDynamicSamplingContextFromClient, @@ -7,6 +6,7 @@ import { getRootSpan, hasTracingEnabled, spanToTraceHeader, + startInactiveSpan, } from '@sentry/core'; import type { Client, HandlerDataFetch, Scope, Span, SpanOrigin } from '@sentry/types'; import { @@ -76,24 +76,21 @@ export function instrumentFetchRequest( const scope = getCurrentScope(); const client = getClient(); - const parentSpan = getActiveSpan(); const { method, url } = handlerData.fetchData; - const span = - shouldCreateSpanResult && parentSpan - ? // eslint-disable-next-line deprecation/deprecation - parentSpan.startChild({ - data: { - url, - type: 'fetch', - 'http.method': method, - }, - description: `${method} ${url}`, - op: 'http.client', - origin: spanOrigin, - }) - : undefined; + const span = shouldCreateSpanResult + ? startInactiveSpan({ + attributes: { + url, + type: 'fetch', + 'http.method': method, + }, + name: `${method} ${url}`, + op: 'http.client', + origin: spanOrigin, + }) + : undefined; if (span) { handlerData.fetchData.__span = span.spanContext().spanId; diff --git a/packages/tracing-internal/test/browser/request.test.ts b/packages/tracing-internal/test/browser/request.test.ts index 782c0890e156..426325072984 100644 --- a/packages/tracing-internal/test/browser/request.test.ts +++ b/packages/tracing-internal/test/browser/request.test.ts @@ -1,32 +1,14 @@ /* eslint-disable deprecation/deprecation */ -import * as sentryCore from '@sentry/core'; -import { Hub, makeMain, spanToJSON } from '@sentry/core'; -import type { HandlerDataFetch, HandlerDataXhr, SentryWrappedXMLHttpRequest } from '@sentry/types'; import * as utils from '@sentry/utils'; -import { SENTRY_XHR_DATA_KEY } from '@sentry/utils'; -import type { Transaction } from '../../../tracing/src'; -import { Span, addExtensionMethods, spanStatusfromHttpCode } from '../../../tracing/src'; -import { getDefaultBrowserClientOptions } from '../../../tracing/test/testutils'; -import { - extractNetworkProtocol, - instrumentOutgoingRequests, - shouldAttachHeaders, - xhrCallback, -} from '../../src/browser/request'; -import { instrumentFetchRequest } from '../../src/common/fetch'; -import { TestClient } from '../utils/TestClient'; +import { extractNetworkProtocol, instrumentOutgoingRequests, shouldAttachHeaders } from '../../src/browser/request'; beforeAll(() => { - addExtensionMethods(); // @ts-expect-error need to override global Request because it's not in the jest environment (even with an // `@jest-environment jsdom` directive, for some reason) global.Request = {}; }); -const hasTracingEnabled = jest.spyOn(sentryCore, 'hasTracingEnabled'); -const setRequestHeader = jest.fn(); - describe('instrumentOutgoingRequests', () => { beforeEach(() => { jest.clearAllMocks(); @@ -59,363 +41,6 @@ describe('instrumentOutgoingRequests', () => { }); }); -describe('callbacks', () => { - let hub: Hub; - let transaction: Transaction; - const alwaysCreateSpan = () => true; - const alwaysAttachHeaders = () => true; - const startTimestamp = 1356996072000; - const endTimestamp = 1356996072000; - - beforeAll(() => { - const options = getDefaultBrowserClientOptions({ tracesSampleRate: 1 }); - hub = new Hub(new TestClient(options)); - makeMain(hub); - }); - - beforeEach(() => { - transaction = hub.startTransaction({ name: 'organizations/users/:userid', op: 'pageload' }) as Transaction; - hub.getScope().setSpan(transaction); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('fetchCallback()', () => { - let fetchHandlerData: HandlerDataFetch; - - const fetchSpan = { - data: { - 'http.method': 'GET', - url: 'http://dogs.are.great/', - type: 'fetch', - }, - description: 'GET http://dogs.are.great/', - op: 'http.client', - parentSpanId: expect.any(String), - spanId: expect.any(String), - startTimestamp: expect.any(Number), - traceId: expect.any(String), - }; - - beforeEach(() => { - fetchHandlerData = { - args: ['http://dogs.are.great/', {}], - fetchData: { url: 'http://dogs.are.great/', method: 'GET' }, - startTimestamp, - }; - }); - - it.each([ - // each case is [shouldCreateSpanReturnValue, shouldAttachHeadersReturnValue, expectedSpan, expectedHeaderKeys] - [true, true, expect.objectContaining(fetchSpan), ['sentry-trace', 'baggage']], - [true, false, expect.objectContaining(fetchSpan), []], - [false, true, undefined, ['sentry-trace', 'baggage']], - [false, false, undefined, []], - ])( - 'span creation/header attachment interaction - shouldCreateSpan: %s, shouldAttachHeaders: %s', - (shouldCreateSpanReturnValue, shouldAttachHeadersReturnValue, expectedSpan, expectedHeaderKeys) => { - instrumentFetchRequest( - fetchHandlerData, - () => shouldCreateSpanReturnValue, - () => shouldAttachHeadersReturnValue, - {}, - ); - - // spans[0] is the transaction itself - const newSpan = transaction.spanRecorder?.spans[1] as Span; - expect(newSpan).toEqual(expectedSpan); - - const headers = (fetchHandlerData.args[1].headers as Record) || {}; - expect(Object.keys(headers)).toEqual(expectedHeaderKeys); - }, - ); - - it('adds neither fetch request spans nor fetch request headers if tracing is disabled', () => { - hasTracingEnabled.mockReturnValueOnce(false); - const spans = {}; - - instrumentFetchRequest(fetchHandlerData, alwaysCreateSpan, alwaysAttachHeaders, spans); - - expect(spans).toEqual({}); - - const headers = (fetchHandlerData.args[1].headers as Record) || {}; - expect(Object.keys(headers)).toEqual([]); - }); - - it('creates and finishes fetch span on active transaction', () => { - const spans = {}; - - // triggered by request being sent - instrumentFetchRequest(fetchHandlerData, alwaysCreateSpan, alwaysAttachHeaders, spans); - - const newSpan = transaction.spanRecorder?.spans[1] as Span; - - expect(newSpan).toBeDefined(); - expect(newSpan).toBeInstanceOf(Span); - expect(newSpan.data).toEqual({ - 'http.method': 'GET', - type: 'fetch', - url: 'http://dogs.are.great/', - }); - expect(newSpan.description).toBe('GET http://dogs.are.great/'); - expect(newSpan.op).toBe('http.client'); - const spanId = fetchHandlerData.fetchData?.__span; - expect(spanId).toBeDefined(); - - const postRequestFetchHandlerData = { - ...fetchHandlerData, - endTimestamp, - }; - - // triggered by response coming back - instrumentFetchRequest(postRequestFetchHandlerData, alwaysCreateSpan, alwaysAttachHeaders, spans); - - expect(spanToJSON(newSpan).timestamp).toBeDefined(); - }); - - it('sets response status on finish', () => { - const spans: Record = {}; - - // triggered by request being sent - instrumentFetchRequest(fetchHandlerData, alwaysCreateSpan, alwaysAttachHeaders, spans); - - const newSpan = transaction.spanRecorder?.spans[1] as Span; - - expect(newSpan).toBeDefined(); - - const postRequestFetchHandlerData = { - ...fetchHandlerData, - endTimestamp, - response: { status: 404 } as Response, - }; - - // triggered by response coming back - instrumentFetchRequest(postRequestFetchHandlerData, alwaysCreateSpan, alwaysAttachHeaders, spans); - - expect(newSpan.status).toBe(spanStatusfromHttpCode(404)); - }); - - it('ignores response with no associated span', () => { - // the request might be missed somehow. E.g. if it was sent before tracing gets enabled. - - const postRequestFetchHandlerData = { - ...fetchHandlerData, - endTimestamp, - response: { status: 404 } as Response, - }; - - // in that case, the response coming back will be ignored - instrumentFetchRequest(postRequestFetchHandlerData, alwaysCreateSpan, alwaysAttachHeaders, {}); - - const newSpan = transaction.spanRecorder?.spans[1]; - - expect(newSpan).toBeUndefined(); - }); - - it('uses active span to generate sentry-trace header', () => { - const spans: Record = {}; - // triggered by request being sent - instrumentFetchRequest(fetchHandlerData, alwaysCreateSpan, alwaysAttachHeaders, spans); - - const activeSpan = transaction.spanRecorder?.spans[1] as Span; - - const postRequestFetchHandlerData = { - ...fetchHandlerData, - endTimestamp, - response: { status: 200 } as Response, - }; - - // triggered by response coming back - instrumentFetchRequest(postRequestFetchHandlerData, alwaysCreateSpan, alwaysAttachHeaders, spans); - - const headers = (fetchHandlerData.args[1].headers as Record) || {}; - expect(headers['sentry-trace']).toEqual(`${activeSpan.traceId}-${activeSpan.spanId}-1`); - }); - - it('adds content-length to span data on finish', () => { - const spans: Record = {}; - - // triggered by request being sent - instrumentFetchRequest(fetchHandlerData, alwaysCreateSpan, alwaysAttachHeaders, spans); - - const newSpan = transaction.spanRecorder?.spans[1] as Span; - - expect(newSpan).toBeDefined(); - - const postRequestFetchHandlerData = { - ...fetchHandlerData, - endTimestamp, - response: { status: 404, headers: { get: () => 123 } }, - } as unknown as HandlerDataFetch; - - // triggered by response coming back - instrumentFetchRequest(postRequestFetchHandlerData, alwaysCreateSpan, alwaysAttachHeaders, spans); - - const finishedSpan = transaction.spanRecorder?.spans[1] as Span; - - expect(finishedSpan).toBeDefined(); - expect(finishedSpan).toBeInstanceOf(Span); - expect(spanToJSON(finishedSpan).data).toEqual({ - 'http.response_content_length': 123, - 'http.method': 'GET', - 'http.response.status_code': 404, - type: 'fetch', - url: 'http://dogs.are.great/', - 'sentry.op': 'http.client', - 'sentry.origin': 'auto.http.browser', - }); - expect(finishedSpan.op).toBe('http.client'); - }); - }); - - describe('xhrCallback()', () => { - let xhrHandlerData: HandlerDataXhr; - - const xhrSpan = { - data: { - 'http.method': 'GET', - url: 'http://dogs.are.great/', - type: 'xhr', - }, - description: 'GET http://dogs.are.great/', - op: 'http.client', - parentSpanId: expect.any(String), - spanId: expect.any(String), - startTimestamp: expect.any(Number), - traceId: expect.any(String), - }; - - beforeEach(() => { - xhrHandlerData = { - args: ['GET', 'http://dogs.are.great/'], - xhr: { - [SENTRY_XHR_DATA_KEY]: { - method: 'GET', - url: 'http://dogs.are.great/', - status_code: 200, - request_headers: {}, - }, - __sentry_xhr_span_id__: '1231201211212012', - setRequestHeader, - } as SentryWrappedXMLHttpRequest, - startTimestamp, - }; - }); - - it.each([ - // each case is [shouldCreateSpanReturnValue, shouldAttachHeadersReturnValue, expectedSpan, expectedHeaderKeys] - [true, true, expect.objectContaining(xhrSpan), ['sentry-trace', 'baggage']], - [true, false, expect.objectContaining(xhrSpan), []], - [false, true, undefined, ['sentry-trace', 'baggage']], - [false, false, undefined, []], - ])( - 'span creation/header attachment interaction - shouldCreateSpan: %s, shouldAttachHeaders: %s', - (shouldCreateSpanReturnValue, shouldAttachHeadersReturnValue, expectedSpan, expectedHeaderKeys) => { - xhrCallback( - xhrHandlerData, - () => shouldCreateSpanReturnValue, - () => shouldAttachHeadersReturnValue, - {}, - ); - - // spans[0] is the transaction itself - const newSpan = transaction.spanRecorder?.spans[1] as Span; - expect(newSpan).toEqual(expectedSpan); - - const headerKeys = setRequestHeader.mock.calls.map(header => header[0]); - expect(headerKeys).toEqual(expectedHeaderKeys); - }, - ); - - it('adds neither xhr request spans nor xhr request headers if tracing is disabled', () => { - hasTracingEnabled.mockReturnValueOnce(false); - const spans = {}; - - xhrCallback(xhrHandlerData, alwaysCreateSpan, alwaysAttachHeaders, spans); - - expect(spans).toEqual({}); - expect(setRequestHeader).not.toHaveBeenCalled(); - }); - - it('creates and finishes XHR span on active transaction', () => { - const spans = {}; - - // triggered by request being sent - xhrCallback(xhrHandlerData, alwaysCreateSpan, alwaysAttachHeaders, spans); - - const newSpan = transaction.spanRecorder?.spans[1] as Span; - - expect(newSpan).toBeInstanceOf(Span); - expect(newSpan.data).toEqual({ - 'http.method': 'GET', - type: 'xhr', - url: 'http://dogs.are.great/', - }); - expect(newSpan.description).toBe('GET http://dogs.are.great/'); - expect(newSpan.op).toBe('http.client'); - const spanId = xhrHandlerData.xhr?.__sentry_xhr_span_id__; - expect(spanId).toBeDefined(); - expect(spanId).toEqual(newSpan?.spanId); - - const postRequestXHRHandlerData = { - ...xhrHandlerData, - endTimestamp, - }; - - // triggered by response coming back - xhrCallback(postRequestXHRHandlerData, alwaysCreateSpan, alwaysAttachHeaders, spans); - - expect(spanToJSON(newSpan).timestamp).toBeDefined(); - }); - - it('sets response status on finish', () => { - const spans = {}; - - // triggered by request being sent - xhrCallback(xhrHandlerData, alwaysCreateSpan, alwaysAttachHeaders, spans); - - const newSpan = transaction.spanRecorder?.spans[1] as Span; - - expect(newSpan).toBeDefined(); - - const postRequestXHRHandlerData = { - ...xhrHandlerData, - endTimestamp, - }; - postRequestXHRHandlerData.xhr![SENTRY_XHR_DATA_KEY]!.status_code = 404; - - // triggered by response coming back - xhrCallback(postRequestXHRHandlerData, alwaysCreateSpan, alwaysAttachHeaders, spans); - - expect(newSpan.status).toBe(spanStatusfromHttpCode(404)); - }); - - it('ignores response with no associated span', () => { - // the request might be missed somehow. E.g. if it was sent before tracing gets enabled. - - const postRequestXHRHandlerData: HandlerDataXhr = { - ...{ - xhr: { - [SENTRY_XHR_DATA_KEY]: xhrHandlerData.xhr?.[SENTRY_XHR_DATA_KEY], - }, - }, - args: ['GET', 'http://dogs.are.great/'], - startTimestamp, - endTimestamp, - }; - - // in that case, the response coming back will be ignored - xhrCallback(postRequestXHRHandlerData, alwaysCreateSpan, alwaysAttachHeaders, {}); - - const newSpan = transaction.spanRecorder?.spans[1]; - - expect(newSpan).toBeUndefined(); - }); - }); -}); - interface ProtocolInfo { name: string; version: string; diff --git a/yarn.lock b/yarn.lock index c02f5da0ff3c..3825958296ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5292,40 +5292,40 @@ magic-string "0.27.0" unplugin "1.0.1" -"@sentry/cli-darwin@2.25.0": - version "2.25.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.25.0.tgz#a46c84852fbecdbd16948548f4c58302cb5471b9" - integrity sha512-OgBioypi9S+cooC4mPj/gYyvjw3oP9TH9ACgzobL0oP9gCpyF36iv044SWHLgeFUb45cPpVZ7f7WeSbufItzCQ== - -"@sentry/cli-linux-arm64@2.25.0": - version "2.25.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.25.0.tgz#dcf61eac6adc6dcc5aee2eaebff2e901370abc75" - integrity sha512-GqxP3s0qHBgch3WI1my5P/h4YeEtNEar+jOGTPg66Bt042rUEHIlYuhULriu3v5rLnmlTuQ5i+LGr4Kq5SFW0Q== - -"@sentry/cli-linux-arm@2.25.0": - version "2.25.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.25.0.tgz#bbea30b44b6b37c7e58d5e47b393d1139cdfe5ef" - integrity sha512-EZT//Dnajc03juqBTRUlU7x/1R1ODq5w6ZC9zO5tJfURxljUJ/kkAScHpfHiqzhPMNArK0gu7vYrOS4CTA7eBw== - -"@sentry/cli-linux-i686@2.25.0": - version "2.25.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.25.0.tgz#b11c4bc6679c253dbd0e000a92198f3ccb41e53c" - integrity sha512-w25QuABMK7FDjlOgpWgJOhQdVQguOhz81DPoeXNWiDLcTHFsYDXxT88exaUQxrLhMNcRrQnS0rDhwd5y0cYh0g== - -"@sentry/cli-linux-x64@2.25.0": - version "2.25.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.25.0.tgz#c2c3be3db81ee08185557fa502ecfb5e516995ab" - integrity sha512-7Pr3JZTPWqSeLiG67v/7uR9prpCfNAW2naf/SSZOMg2ZTXSgG+kgXf6/ADI3WP1LtF3GVhexGtJ5eyFVYxfLsQ== - -"@sentry/cli-win32-i686@2.25.0": - version "2.25.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.25.0.tgz#968f5093e3a74401fe15233808aee68311d15496" - integrity sha512-AuHBpFB2DZr19KE3g7qejaVmGb0d7E4ZN2cBKX1Vixb+KTi9/bEcRrWaQ2PpqLTVb2Wwglf/VlZgsxOjfhp7Ag== - -"@sentry/cli-win32-x64@2.25.0": - version "2.25.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.25.0.tgz#9bb6b271642e1cc2b571acd395bece2683a3faed" - integrity sha512-EFGg2L4Wm8YNRV/yAy0bmztc2jkkhy0SfaQtxrHW22IRqfl2jXyKcHHmcEjwoYfCvIW+c5I0ftHhjueMuuRcXw== +"@sentry/cli-darwin@2.26.0": + version "2.26.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.26.0.tgz#6b44500dd549415c5f8b7228a3f4aef18a2e6766" + integrity sha512-SJ4ts9VELoLdOx1g034Tv2nGqhjutBYNAI3WMsjBaQG3tqNPJkQJKGrOqfpL6kTdO2tqQIAYeVw60yqWuHU3FA== + +"@sentry/cli-linux-arm64@2.26.0": + version "2.26.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.26.0.tgz#dfe28a7caeffd8bd68476b709ba5f5f50fa74244" + integrity sha512-tAsK5pWrLyU+zqoW0uwylfLB7udOV8FtU8xqcfMsYGxt44zviiuxzKeDnaUdHsZcvk03aTAyf1Dxqn0u32A0MA== + +"@sentry/cli-linux-arm@2.26.0": + version "2.26.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.26.0.tgz#cae4cf7db31a0cb4dba5243fce826abfa71dc404" + integrity sha512-qNqKLf3eGowhm+4gg47jGLfova5SLgC0wvWX181U+w94oVGp4onuSjbqpy7wbSA9nsfTXllMhEFI5jA4CMmZVw== + +"@sentry/cli-linux-i686@2.26.0": + version "2.26.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.26.0.tgz#29cd6617d2e764dec4cff377ede58762b87e9a66" + integrity sha512-+dSFR9rK6o6F0gBxoU0mrHw18qVgF1t27Y0jvdItMtDuCuduBuXTffmsbBwbPFWBgWuLPG+ojB1LuoBt5qVMng== + +"@sentry/cli-linux-x64@2.26.0": + version "2.26.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.26.0.tgz#a54dbcdd5c377ba97f7d3cc8115f25e68c3aded4" + integrity sha512-oY86ECWVQuk434K+enUVZnn28T8qxjJTpxN079xvz7SIWOxQ609tMva91Ywo0gExcu07AZ0pg71XFsEQ9WhZgA== + +"@sentry/cli-win32-i686@2.26.0": + version "2.26.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.26.0.tgz#42fdf7006e9e420cb0b3e6b70d9f44f6807906e9" + integrity sha512-vLju9NFl4venKEVpuFJpxaCwa2NdG6C9mhYNqxRvZAPrXWMdMd697qBDOMepAPT7CI8EWiyXUwMli0WjGXGMeQ== + +"@sentry/cli-win32-x64@2.26.0": + version "2.26.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.26.0.tgz#805cb184652d4a608128b70f9e19c1117f25c6f6" + integrity sha512-r3ZaxdHGC6OyJhOxO5ADzAitpGcgT/PkqQzOzKXBOebHj5jzwY27JWjdNhpT6sJZDII13HxqwISRedVWftZgRw== "@sentry/cli@^1.74.4", "@sentry/cli@^1.77.1": version "1.77.1" @@ -5340,9 +5340,9 @@ which "^2.0.2" "@sentry/cli@^2.17.0", "@sentry/cli@^2.21.2", "@sentry/cli@^2.23.0": - version "2.25.0" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.25.0.tgz#2b142b763d21def99a3473f0cd3169f6b04f6235" - integrity sha512-N7k3NdiiEyQkQ43hRDAVqMf+Lg3GTWevO+ndg4yZ8Zv+J1jEVD6ZbqNnshSwWOx9qzcWQ+V/8ZgjmNuHbcNRxg== + version "2.26.0" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.26.0.tgz#94cee60c89f457318540f74f8f1158357c2dd706" + integrity sha512-WRrY9nkjLLUvyo+l8KE0x0Q+0NtCd2U8HYJzh3kyJHyyfKWiSH7ZhExcsb2MoSIjlzbKjjrIJzxhklZABkidDw== dependencies: https-proxy-agent "^5.0.0" node-fetch "^2.6.7" @@ -5350,13 +5350,13 @@ proxy-from-env "^1.1.0" which "^2.0.2" optionalDependencies: - "@sentry/cli-darwin" "2.25.0" - "@sentry/cli-linux-arm" "2.25.0" - "@sentry/cli-linux-arm64" "2.25.0" - "@sentry/cli-linux-i686" "2.25.0" - "@sentry/cli-linux-x64" "2.25.0" - "@sentry/cli-win32-i686" "2.25.0" - "@sentry/cli-win32-x64" "2.25.0" + "@sentry/cli-darwin" "2.26.0" + "@sentry/cli-linux-arm" "2.26.0" + "@sentry/cli-linux-arm64" "2.26.0" + "@sentry/cli-linux-i686" "2.26.0" + "@sentry/cli-linux-x64" "2.26.0" + "@sentry/cli-win32-i686" "2.26.0" + "@sentry/cli-win32-x64" "2.26.0" "@sentry/vite-plugin@^0.6.1": version "0.6.1" @@ -5739,6 +5739,13 @@ dependencies: bson "*" +"@types/bson@1.x || 4.0.x": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/bson/-/bson-4.0.5.tgz#9e0e1d1a6f8866483f96868a9b33bc804926b1fc" + integrity sha512-vVLwMUqhYJSQ/WKcE60eFqcyuWse5fGH+NMAXHuKrUAPoryq3ATxk5o4bgYNtg5aOM4APVg7Hnb3ASqUYG0PKg== + dependencies: + "@types/node" "*" + "@types/chai-as-promised@^7.1.2": version "7.1.3" resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-7.1.3.tgz#779166b90fda611963a3adbfd00b339d03b747bd" @@ -6268,6 +6275,11 @@ resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.0.tgz#57f228f2b80c046b4a1bd5cac031f81f207f4f03" integrity sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w== +"@types/luxon@~3.3.0": + version "3.3.8" + resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.3.8.tgz#84dbf2d020a9209a272058725e168f21d331a67e" + integrity sha512-jYvz8UMLDgy3a5SkGJne8H7VA7zPV2Lwohjx0V8V31+SqAjNmurWMkk9cQhfvlcnXWudBpK9xPM1n4rljOcHYQ== + "@types/md5@2.1.33": version "2.1.33" resolved "https://registry.yarnpkg.com/@types/md5/-/md5-2.1.33.tgz#8c8dba30df4ad0e92296424f08c4898dd808e8df" @@ -6309,7 +6321,7 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.1.tgz#283f669ff76d7b8260df8ab7a4262cc83d988256" integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg== -"@types/mongodb@^3.6.20": +"@types/mongodb@^3.5.27", "@types/mongodb@^3.6.20": version "3.6.20" resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.6.20.tgz#b7c5c580644f6364002b649af1c06c3c0454e1d2" integrity sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ== @@ -9535,6 +9547,11 @@ blank-object@^1.0.1: resolved "https://registry.yarnpkg.com/blank-object/-/blank-object-1.0.2.tgz#f990793fbe9a8c8dd013fb3219420bec81d5f4b9" integrity sha1-+ZB5P76ajI3QE/syGUIL7IHV9Lk= +bluebird@3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" + integrity sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA== + bluebird@^3.4.6, bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" @@ -11947,6 +11964,14 @@ critters@0.0.12: postcss "^8.3.7" pretty-bytes "^5.3.0" +cron@^3.1.6: + version "3.1.6" + resolved "https://registry.yarnpkg.com/cron/-/cron-3.1.6.tgz#e7e1798a468e017c8d31459ecd7c2d088f97346c" + integrity sha512-cvFiQCeVzsA+QPM6fhjBtlKGij7tLLISnTSvFxVdnFGLdz+ZdXN37kNe0i2gefmdD17XuZA6n2uPVwzl4FxW/w== + dependencies: + "@types/luxon" "~3.3.0" + luxon "~3.4.0" + cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -19616,6 +19641,11 @@ jws@^4.0.0: jwa "^2.0.0" safe-buffer "^5.0.1" +kareem@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.2.tgz#78c4508894985b8d38a0dc15e1a8e11078f2ca93" + integrity sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ== + karma-browserstack-launcher@^1.5.1: version "1.6.0" resolved "https://registry.yarnpkg.com/karma-browserstack-launcher/-/karma-browserstack-launcher-1.6.0.tgz#2f6000647073e77ae296653b8830b279669766ef" @@ -20696,6 +20726,11 @@ lunr@^2.3.8: resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow== +luxon@~3.4.0: + version "3.4.4" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.4.4.tgz#cf20dc27dc532ba41a169c43fdcc0063601577af" + integrity sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA== + lz-string@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" @@ -22120,6 +22155,19 @@ mongodb-memory-server-global@^7.6.3: mongodb-memory-server-core "7.6.3" tslib "^2.3.0" +mongodb@3.7.4: + version "3.7.4" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.7.4.tgz#119530d826361c3e12ac409b769796d6977037a4" + integrity sha512-K5q8aBqEXMwWdVNh94UQTwZ6BejVbFhh1uB6c5FKtPE9eUMZPUO3sRZdgIEcHSrAWmxzpG/FeODDKL388sqRmw== + dependencies: + bl "^2.2.1" + bson "^1.1.4" + denque "^1.4.1" + optional-require "^1.1.8" + safe-buffer "^5.1.2" + optionalDependencies: + saslprep "^1.0.0" + mongodb@^3.7.3: version "3.7.3" resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.7.3.tgz#b7949cfd0adc4cc7d32d3f2034214d4475f175a5" @@ -22133,6 +22181,31 @@ mongodb@^3.7.3: optionalDependencies: saslprep "^1.0.0" +mongoose-legacy-pluralize@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4" + integrity sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ== + +mongoose@^5.13.22: + version "5.13.22" + resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.13.22.tgz#f9a6493ba5f45b7a3d5f9fce58ca9c71aedb8157" + integrity sha512-p51k/c4X/MfqeQ3I1ranlDiggLzNumZrTDD9CeezHwZxt2/btf+YZD7MCe07RAY2NgFYVMayq6jMamw02Jmf9w== + dependencies: + "@types/bson" "1.x || 4.0.x" + "@types/mongodb" "^3.5.27" + bson "^1.1.4" + kareem "2.3.2" + mongodb "3.7.4" + mongoose-legacy-pluralize "1.0.2" + mpath "0.8.4" + mquery "3.2.5" + ms "2.1.2" + optional-require "1.0.x" + regexp-clone "1.0.0" + safe-buffer "5.2.1" + sift "13.5.2" + sliced "1.0.1" + morgan@^1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" @@ -22161,6 +22234,22 @@ move-concurrently@^1.0.1: rimraf "^2.5.4" run-queue "^1.0.3" +mpath@0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.8.4.tgz#6b566d9581621d9e931dd3b142ed3618e7599313" + integrity sha512-DTxNZomBcTWlrMW76jy1wvV37X/cNNxPW1y2Jzd4DZkAaC5ZGsm8bfGfNOthcDuRJujXLqiuS6o3Tpy0JEoh7g== + +mquery@3.2.5: + version "3.2.5" + resolved "https://registry.yarnpkg.com/mquery/-/mquery-3.2.5.tgz#8f2305632e4bb197f68f60c0cffa21aaf4060c51" + integrity sha512-VjOKHHgU84wij7IUoZzFRU07IAxd5kWJaDmyUzQlbjHjyoeK5TNeeo8ZsFDtTYnSgpW6n/nMNIHvE3u8Lbrf4A== + dependencies: + bluebird "3.5.1" + debug "3.1.0" + regexp-clone "^1.0.0" + safe-buffer "5.1.2" + sliced "1.0.1" + mri@1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.4.tgz#7cb1dd1b9b40905f1fac053abe25b6720f44744a" @@ -23488,6 +23577,11 @@ opn@^5.5.0: dependencies: is-wsl "^1.1.0" +optional-require@1.0.x: + version "1.0.3" + resolved "https://registry.yarnpkg.com/optional-require/-/optional-require-1.0.3.tgz#275b8e9df1dc6a17ad155369c2422a440f89cb07" + integrity sha512-RV2Zp2MY2aeYK5G+B/Sps8lW5NHAzE5QClbFP15j+PWmP+T9PxlJXBOOLoSAdgwFvS4t0aMR4vpedMkbHfh0nA== + optional-require@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/optional-require/-/optional-require-1.1.8.tgz#16364d76261b75d964c482b2406cb824d8ec44b7" @@ -26650,6 +26744,11 @@ regex-parser@^2.2.11: resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.11.tgz#3b37ec9049e19479806e878cabe7c1ca83ccfe58" integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q== +regexp-clone@1.0.0, regexp-clone@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-1.0.0.tgz#222db967623277056260b992626354a04ce9bf63" + integrity sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw== + regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" @@ -28042,6 +28141,11 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" +sift@13.5.2: + version "13.5.2" + resolved "https://registry.yarnpkg.com/sift/-/sift-13.5.2.tgz#24a715e13c617b086166cd04917d204a591c9da6" + integrity sha512-+gxdEOMA2J+AI+fVsCqeNn7Tgx3M9ZN9jdi95939l1IJ8cZsqS8sqpJyOkic2SJk+1+98Uwryt/gL6XDaV+UZA== + siginfo@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" @@ -28212,6 +28316,11 @@ slice-ansi@^5.0.0: ansi-styles "^6.0.0" is-fullwidth-code-point "^4.0.0" +sliced@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" + integrity sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA== + smart-buffer@^4.1.0, smart-buffer@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"