From 40e4470fd41900784f84193915fea15f5998d5a1 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 17 Oct 2023 12:18:50 +0200 Subject: [PATCH 01/38] build(deno): Clean up build output (#9276) --- packages/deno/.gitignore | 1 + packages/deno/package.json | 2 +- packages/deno/rollup.types.config.js | 8 +++++--- packages/deno/tsconfig.types.json | 4 ++-- 4 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 packages/deno/.gitignore diff --git a/packages/deno/.gitignore b/packages/deno/.gitignore new file mode 100644 index 000000000000..299ae4a5c2fd --- /dev/null +++ b/packages/deno/.gitignore @@ -0,0 +1 @@ +build-types diff --git a/packages/deno/package.json b/packages/deno/package.json index 59abec8475aa..640c7400250b 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -35,7 +35,7 @@ "build:types:tsc": "tsc -p tsconfig.types.json", "build:types:bundle": "rollup -c rollup.types.config.js", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build coverage", + "clean": "rimraf build build-types coverage", "prefix": "yarn deno-types", "fix": "run-s fix:eslint fix:prettier", "fix:eslint": "eslint . --format stylish --fix", diff --git a/packages/deno/rollup.types.config.js b/packages/deno/rollup.types.config.js index d8123b6c5cd3..a14c531d555f 100644 --- a/packages/deno/rollup.types.config.js +++ b/packages/deno/rollup.types.config.js @@ -1,7 +1,9 @@ +// @ts-check import dts from 'rollup-plugin-dts'; +import { defineConfig } from 'rollup'; -export default { - input: './build/index.d.ts', +export default defineConfig({ + input: './build-types/index.d.ts', output: [{ file: 'build/index.d.ts', format: 'es' }], plugins: [ dts({ respectExternal: true }), @@ -14,4 +16,4 @@ export default { }, }, ], -}; +}); diff --git a/packages/deno/tsconfig.types.json b/packages/deno/tsconfig.types.json index d6d1e9a548c9..b4181d30685e 100644 --- a/packages/deno/tsconfig.types.json +++ b/packages/deno/tsconfig.types.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "declaration": true, - "declarationMap": true, + "declarationMap": false, "emitDeclarationOnly": true, - "outDir": "build" + "outDir": "build-types" } } From 2a08a79617d48ac0262933c1aadaeb352d3904ca Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 17 Oct 2023 14:45:57 +0200 Subject: [PATCH 02/38] build(ember): Gitignore prepack artifacts (#9282) --- packages/ember/.gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/ember/.gitignore b/packages/ember/.gitignore index 59a00ea13675..07c47279da5c 100644 --- a/packages/ember/.gitignore +++ b/packages/ember/.gitignore @@ -29,3 +29,9 @@ # broccoli-debug /DEBUG/ + +# These get created when packaging +/instance-initializers +index.d.ts +runloop.d.ts +types.d.ts From dfe5db59c2ce66aa32ba0352ee25a252ac1c70de Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 17 Oct 2023 15:25:47 +0200 Subject: [PATCH 03/38] build(deno): Prepare Deno SDK for release on npm (#9281) --- .craft.yml | 3 +++ .github/workflows/build.yml | 6 ++++++ packages/deno/package.json | 10 ++++++++-- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.craft.yml b/.craft.yml index 39a52c69437a..039528edc0a9 100644 --- a/.craft.yml +++ b/.craft.yml @@ -63,6 +63,9 @@ targets: - name: npm id: '@sentry/vercel-edge' includeNames: /^sentry-vercel-edge-\d.*\.tgz$/ + - name: npm + id: '@sentry/deno' + includeNames: /^sentry-deno-\d.*\.tgz$/ ## 5. Node-based Packages - name: npm diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e233d1dec828..66b282202f5e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -122,6 +122,10 @@ jobs: - *shared - 'packages/node/**' - 'packages/node-integration-tests/**' + deno: + - *shared + - *browser + - 'packages/deno/**' any_code: - '!**/*.md' @@ -135,6 +139,7 @@ jobs: changed_ember: ${{ steps.changed.outputs.ember }} changed_remix: ${{ steps.changed.outputs.remix }} changed_node: ${{ steps.changed.outputs.node }} + changed_deno: ${{ steps.changed.outputs.deno }} changed_browser: ${{ steps.changed.outputs.browser }} changed_browser_integration: ${{ steps.changed.outputs.browser_integration }} changed_any_code: ${{ steps.changed.outputs.any_code }} @@ -422,6 +427,7 @@ jobs: job_deno_unit_tests: name: Deno Unit Tests needs: [job_get_metadata, job_build] + if: needs.job_get_metadata.outputs.changed_deno == 'true' || github.event_name != 'pull_request' timeout-minutes: 10 runs-on: ubuntu-20.04 strategy: diff --git a/packages/deno/package.json b/packages/deno/package.json index 58c9a2534d24..75f8526efca4 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -9,10 +9,14 @@ "main": "build/index.js", "module": "build/index.js", "types": "build/index.d.ts", - "private": true, "publishConfig": { "access": "public" }, + "files": [ + "index.js", + "index.js.map", + "index.d.ts" + ], "dependencies": { "@sentry/browser": "7.74.1", "@sentry/core": "7.74.1", @@ -34,6 +38,7 @@ "build:types": "run-s deno-types build:types:tsc build:types:bundle", "build:types:tsc": "tsc -p tsconfig.types.json", "build:types:bundle": "rollup -c rollup.types.config.js", + "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build build-types coverage", "prefix": "yarn deno-types", @@ -48,7 +53,8 @@ "test": "run-s deno-types install:deno test:types test:unit", "test:types": "deno check ./build/index.js", "test:unit": "deno test --allow-read --allow-run", - "test:unit:update": "deno test --allow-read --allow-write --allow-run -- --update" + "test:unit:update": "deno test --allow-read --allow-write --allow-run -- --update", + "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" }, "volta": { "extends": "../../package.json" From 441f702bc76aa76f9413e35f7c22e9369427511f Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 17 Oct 2023 18:23:46 +0200 Subject: [PATCH 04/38] fix(astro): Make `Replay` and `BrowserTracing` integrations tree-shakeable (#9287) Fix a Sentry.init code generation bug that always added `BrowserTracing` and `Replay` to the client init code, even if sample rates were specifically set to 0. This prohibited tree shaking of these integrations if users disabled them by setting the respective sample rates. --- packages/astro/src/integration/snippets.ts | 28 ++++++++++++++- .../astro/test/integration/snippets.test.ts | 36 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/astro/src/integration/snippets.ts b/packages/astro/src/integration/snippets.ts index 28d03ea443eb..3b732b55a330 100644 --- a/packages/astro/src/integration/snippets.ts +++ b/packages/astro/src/integration/snippets.ts @@ -16,7 +16,7 @@ export function buildClientSnippet(options: SentryOptions): string { Sentry.init({ ${buildCommonInitOptions(options)} - integrations: [new Sentry.BrowserTracing(), new Sentry.Replay()], + integrations: [${buildClientIntegrations(options)}], replaysSessionSampleRate: ${options.replaysSessionSampleRate ?? 0.1}, replaysOnErrorSampleRate: ${options.replaysOnErrorSampleRate ?? 1.0}, });`; @@ -43,3 +43,29 @@ const buildCommonInitOptions = (options: SentryOptions): string => `dsn: ${ tracesSampleRate: ${options.tracesSampleRate ?? 1.0},${ options.sampleRate ? `\n sampleRate: ${options.sampleRate},` : '' }`; + +/** + * We don't include the `BrowserTracing` integration if the tracesSampleRate is set to 0. + * Likewise, we don't include the `Replay` integration if the replaysSessionSampleRate + * and replaysOnErrorSampleRate are set to 0. + * + * This way, we avoid unnecessarily adding the integrations and thereby enable tree shaking of the integrations. + */ +const buildClientIntegrations = (options: SentryOptions): string => { + const integrations: string[] = []; + + if (options.tracesSampleRate == null || options.tracesSampleRate) { + integrations.push('new Sentry.BrowserTracing()'); + } + + if ( + options.replaysSessionSampleRate == null || + options.replaysSessionSampleRate || + options.replaysOnErrorSampleRate == null || + options.replaysOnErrorSampleRate + ) { + integrations.push('new Sentry.Replay()'); + } + + return integrations.join(', '); +}; diff --git a/packages/astro/test/integration/snippets.test.ts b/packages/astro/test/integration/snippets.test.ts index 60406b652bf8..172756847a5c 100644 --- a/packages/astro/test/integration/snippets.test.ts +++ b/packages/astro/test/integration/snippets.test.ts @@ -49,6 +49,42 @@ describe('buildClientSnippet', () => { });" `); }); + + it('does not include BrowserTracing if tracesSampleRate is 0', () => { + const snippet = buildClientSnippet({ tracesSampleRate: 0 }); + expect(snippet).toMatchInlineSnapshot(` + "import * as Sentry from \\"@sentry/astro\\"; + + Sentry.init({ + dsn: import.meta.env.PUBLIC_SENTRY_DSN, + debug: false, + environment: import.meta.env.PUBLIC_VERCEL_ENV, + release: import.meta.env.PUBLIC_VERCEL_GIT_COMMIT_SHA, + tracesSampleRate: 0, + integrations: [new Sentry.Replay()], + replaysSessionSampleRate: 0.1, + replaysOnErrorSampleRate: 1, + });" + `); + }); +}); + +it('does not include Replay if replay sample ratest are 0', () => { + const snippet = buildClientSnippet({ replaysSessionSampleRate: 0, replaysOnErrorSampleRate: 0 }); + expect(snippet).toMatchInlineSnapshot(` + "import * as Sentry from \\"@sentry/astro\\"; + + Sentry.init({ + dsn: import.meta.env.PUBLIC_SENTRY_DSN, + debug: false, + environment: import.meta.env.PUBLIC_VERCEL_ENV, + release: import.meta.env.PUBLIC_VERCEL_GIT_COMMIT_SHA, + tracesSampleRate: 1, + integrations: [new Sentry.BrowserTracing()], + replaysSessionSampleRate: 0, + replaysOnErrorSampleRate: 0, + });" + `); }); describe('buildServerSnippet', () => { From df08e8fdffb99e3940cac167595e3b96cc41f955 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 17 Oct 2023 21:35:20 +0200 Subject: [PATCH 05/38] feat(node): Add abnormal session support for ANR (#9268) --- packages/core/src/session.ts | 5 ++ .../suites/anr/basic-session.js | 31 +++++++++ .../suites/anr/basic.js | 8 +-- .../suites/anr/basic.mjs | 8 +-- .../suites/anr/forked.js | 8 +-- .../suites/anr/test-transport.js | 17 +++++ .../node-integration-tests/suites/anr/test.ts | 69 ++++++++++++++----- packages/node/src/anr/index.ts | 69 ++++++++++++++----- packages/types/src/session.ts | 3 +- 9 files changed, 168 insertions(+), 50 deletions(-) create mode 100644 packages/node-integration-tests/suites/anr/basic-session.js create mode 100644 packages/node-integration-tests/suites/anr/test-transport.js diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 2987f09addb5..4c82ef60ffd4 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -57,6 +57,10 @@ export function updateSession(session: Session, context: SessionContext = {}): v session.timestamp = context.timestamp || timestampInSeconds(); + if (context.abnormal_mechanism) { + session.abnormal_mechanism = context.abnormal_mechanism; + } + if (context.ignoreDuration) { session.ignoreDuration = context.ignoreDuration; } @@ -143,6 +147,7 @@ function sessionToJSON(session: Session): SerializedSession { errors: session.errors, did: typeof session.did === 'number' || typeof session.did === 'string' ? `${session.did}` : undefined, duration: session.duration, + abnormal_mechanism: session.abnormal_mechanism, attrs: { release: session.release, environment: session.environment, diff --git a/packages/node-integration-tests/suites/anr/basic-session.js b/packages/node-integration-tests/suites/anr/basic-session.js new file mode 100644 index 000000000000..29cdc17e76c9 --- /dev/null +++ b/packages/node-integration-tests/suites/anr/basic-session.js @@ -0,0 +1,31 @@ +const crypto = require('crypto'); + +const Sentry = require('@sentry/node'); + +const { transport } = require('./test-transport.js'); + +// close both processes after 5 seconds +setTimeout(() => { + process.exit(); +}, 5000); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + debug: true, + transport, +}); + +Sentry.enableAnrDetection({ captureStackTrace: true, anrThreshold: 200 }).then(() => { + function longWork() { + for (let i = 0; i < 100; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + // eslint-disable-next-line no-unused-vars + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + } + } + + setTimeout(() => { + longWork(); + }, 1000); +}); diff --git a/packages/node-integration-tests/suites/anr/basic.js b/packages/node-integration-tests/suites/anr/basic.js index 45a324e507c5..33c4151a19f1 100644 --- a/packages/node-integration-tests/suites/anr/basic.js +++ b/packages/node-integration-tests/suites/anr/basic.js @@ -2,6 +2,8 @@ const crypto = require('crypto'); const Sentry = require('@sentry/node'); +const { transport } = require('./test-transport.js'); + // close both processes after 5 seconds setTimeout(() => { process.exit(); @@ -11,10 +13,8 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', debug: true, - beforeSend: event => { - // eslint-disable-next-line no-console - console.log(JSON.stringify(event)); - }, + autoSessionTracking: false, + transport, }); Sentry.enableAnrDetection({ captureStackTrace: true, anrThreshold: 200 }).then(() => { diff --git a/packages/node-integration-tests/suites/anr/basic.mjs b/packages/node-integration-tests/suites/anr/basic.mjs index 1d89ac1b3989..3d10dc556076 100644 --- a/packages/node-integration-tests/suites/anr/basic.mjs +++ b/packages/node-integration-tests/suites/anr/basic.mjs @@ -2,6 +2,8 @@ import * as crypto from 'crypto'; import * as Sentry from '@sentry/node'; +const { transport } = await import('./test-transport.js'); + // close both processes after 5 seconds setTimeout(() => { process.exit(); @@ -11,10 +13,8 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', debug: true, - beforeSend: event => { - // eslint-disable-next-line no-console - console.log(JSON.stringify(event)); - }, + autoSessionTracking: false, + transport, }); await Sentry.enableAnrDetection({ captureStackTrace: true, anrThreshold: 200 }); diff --git a/packages/node-integration-tests/suites/anr/forked.js b/packages/node-integration-tests/suites/anr/forked.js index 45a324e507c5..33c4151a19f1 100644 --- a/packages/node-integration-tests/suites/anr/forked.js +++ b/packages/node-integration-tests/suites/anr/forked.js @@ -2,6 +2,8 @@ const crypto = require('crypto'); const Sentry = require('@sentry/node'); +const { transport } = require('./test-transport.js'); + // close both processes after 5 seconds setTimeout(() => { process.exit(); @@ -11,10 +13,8 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', debug: true, - beforeSend: event => { - // eslint-disable-next-line no-console - console.log(JSON.stringify(event)); - }, + autoSessionTracking: false, + transport, }); Sentry.enableAnrDetection({ captureStackTrace: true, anrThreshold: 200 }).then(() => { diff --git a/packages/node-integration-tests/suites/anr/test-transport.js b/packages/node-integration-tests/suites/anr/test-transport.js new file mode 100644 index 000000000000..86836cd6ab35 --- /dev/null +++ b/packages/node-integration-tests/suites/anr/test-transport.js @@ -0,0 +1,17 @@ +const { TextEncoder, TextDecoder } = require('util'); + +const { createTransport } = require('@sentry/core'); +const { parseEnvelope } = require('@sentry/utils'); + +const textEncoder = new TextEncoder(); +const textDecoder = new TextDecoder(); + +// A transport that just logs the envelope payloads to console for checking in tests +exports.transport = () => { + return createTransport({ recordDroppedEvent: () => {}, textEncoder }, async request => { + const env = parseEnvelope(request.body, textEncoder, textDecoder); + // eslint-disable-next-line no-console + console.log(JSON.stringify(env[1][0][1])); + return { statusCode: 200 }; + }); +}; diff --git a/packages/node-integration-tests/suites/anr/test.ts b/packages/node-integration-tests/suites/anr/test.ts index 96d83c64a6a7..e7214ae194ec 100644 --- a/packages/node-integration-tests/suites/anr/test.ts +++ b/packages/node-integration-tests/suites/anr/test.ts @@ -1,4 +1,5 @@ import type { Event } from '@sentry/node'; +import type { SerializedSession } from '@sentry/types'; import { parseSemver } from '@sentry/utils'; import * as childProcess from 'child_process'; import * as path from 'path'; @@ -6,19 +7,21 @@ import * as path from 'path'; const NODE_VERSION = parseSemver(process.versions.node).major || 0; /** The output will contain logging so we need to find the line that parses as JSON */ -function parseJsonLine(input: string): T { - return ( - input - .split('\n') - .map(line => { - try { - return JSON.parse(line) as T; - } catch { - return undefined; - } - }) - .filter(a => a) as T[] - )[0]; +function parseJsonLines(input: string, expected: number): T { + const results = input + .split('\n') + .map(line => { + try { + return JSON.parse(line) as T; + } catch { + return undefined; + } + }) + .filter(a => a) as T; + + expect(results.length).toEqual(expected); + + return results; } describe('should report ANR when event loop blocked', () => { @@ -26,12 +29,12 @@ describe('should report ANR when event loop blocked', () => { // The stack trace is different when node < 12 const testFramesDetails = NODE_VERSION >= 12; - expect.assertions(testFramesDetails ? 6 : 4); + expect.assertions(testFramesDetails ? 7 : 5); const testScriptPath = path.resolve(__dirname, 'basic.js'); childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => { - const event = parseJsonLine(stdout); + const [event] = parseJsonLines<[Event]>(stdout, 1); expect(event.exception?.values?.[0].mechanism).toEqual({ type: 'ANR' }); expect(event.exception?.values?.[0].type).toEqual('ApplicationNotResponding'); @@ -53,12 +56,12 @@ describe('should report ANR when event loop blocked', () => { return; } - expect.assertions(6); + expect.assertions(7); const testScriptPath = path.resolve(__dirname, 'basic.mjs'); childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => { - const event = parseJsonLine(stdout); + const [event] = parseJsonLines<[Event]>(stdout, 1); expect(event.exception?.values?.[0].mechanism).toEqual({ type: 'ANR' }); expect(event.exception?.values?.[0].type).toEqual('ApplicationNotResponding'); @@ -71,16 +74,44 @@ describe('should report ANR when event loop blocked', () => { }); }); + test('With session', done => { + // The stack trace is different when node < 12 + const testFramesDetails = NODE_VERSION >= 12; + + expect.assertions(testFramesDetails ? 9 : 7); + + const testScriptPath = path.resolve(__dirname, 'basic-session.js'); + + childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => { + const [session, event] = parseJsonLines<[SerializedSession, Event]>(stdout, 2); + + expect(event.exception?.values?.[0].mechanism).toEqual({ type: 'ANR' }); + expect(event.exception?.values?.[0].type).toEqual('ApplicationNotResponding'); + expect(event.exception?.values?.[0].value).toEqual('Application Not Responding for at least 200 ms'); + expect(event.exception?.values?.[0].stacktrace?.frames?.length).toBeGreaterThan(4); + + if (testFramesDetails) { + expect(event.exception?.values?.[0].stacktrace?.frames?.[2].function).toEqual('?'); + expect(event.exception?.values?.[0].stacktrace?.frames?.[3].function).toEqual('longWork'); + } + + expect(session.status).toEqual('abnormal'); + expect(session.abnormal_mechanism).toEqual('anr_foreground'); + + done(); + }); + }); + test('from forked process', done => { // The stack trace is different when node < 12 const testFramesDetails = NODE_VERSION >= 12; - expect.assertions(testFramesDetails ? 6 : 4); + expect.assertions(testFramesDetails ? 7 : 5); const testScriptPath = path.resolve(__dirname, 'forker.js'); childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => { - const event = parseJsonLine(stdout); + const [event] = parseJsonLines<[Event]>(stdout, 1); expect(event.exception?.values?.[0].mechanism).toEqual({ type: 'ANR' }); expect(event.exception?.values?.[0].type).toEqual('ApplicationNotResponding'); diff --git a/packages/node/src/anr/index.ts b/packages/node/src/anr/index.ts index 9c6827c38259..caa384b3928f 100644 --- a/packages/node/src/anr/index.ts +++ b/packages/node/src/anr/index.ts @@ -1,8 +1,9 @@ -import type { Event, StackFrame } from '@sentry/types'; +import { makeSession, updateSession } from '@sentry/core'; +import type { Event, Session, StackFrame } from '@sentry/types'; import { logger, watchdogTimer } from '@sentry/utils'; import { spawn } from 'child_process'; -import { addGlobalEventProcessor, captureEvent, flush } from '..'; +import { addGlobalEventProcessor, captureEvent, flush, getCurrentHub } from '..'; import { captureStackTrace } from './debugger'; const DEFAULT_INTERVAL = 50; @@ -41,8 +42,8 @@ interface Options { debug: boolean; } -function sendEvent(blockedMs: number, frames?: StackFrame[]): void { - const event: Event = { +function createAnrEvent(blockedMs: number, frames?: StackFrame[]): Event { + return { level: 'error', exception: { values: [ @@ -58,13 +59,6 @@ function sendEvent(blockedMs: number, frames?: StackFrame[]): void { ], }, }; - - captureEvent(event); - - void flush(3000).then(() => { - // We only capture one event to avoid spamming users with errors - process.exit(); - }); } interface InspectorApi { @@ -97,6 +91,8 @@ function startChildProcess(options: Options): void { logger.log(`[ANR] ${message}`, ...args); } + const hub = getCurrentHub(); + try { const env = { ...process.env }; env.SENTRY_ANR_CHILD_PROCESS = 'true'; @@ -105,7 +101,7 @@ function startChildProcess(options: Options): void { env.SENTRY_INSPECT_URL = startInspector(); } - log(`Spawning child process with execPath:'${process.execPath}' and entryScript'${options.entryScript}'`); + log(`Spawning child process with execPath:'${process.execPath}' and entryScript:'${options.entryScript}'`); const child = spawn(process.execPath, [options.entryScript], { env, @@ -116,13 +112,24 @@ function startChildProcess(options: Options): void { const timer = setInterval(() => { try { + const currentSession = hub.getScope()?.getSession(); + // We need to copy the session object and remove the toJSON method so it can be sent to the child process + // serialized without making it a SerializedSession + const session = currentSession ? { ...currentSession, toJSON: undefined } : undefined; // message the child process to tell it the main event loop is still running - child.send('ping'); + child.send({ session }); } catch (_) { // } }, options.pollInterval); + child.on('message', (msg: string) => { + if (msg === 'session-ended') { + log('ANR event sent from child process. Clearing session in this process.'); + hub.getScope()?.setSession(undefined); + } + }); + const end = (type: string): ((...args: unknown[]) => void) => { return (...args): void => { clearInterval(timer); @@ -153,13 +160,36 @@ function createHrTimer(): { getTimeMs: () => number; reset: () => void } { } function handleChildProcess(options: Options): void { + process.title = 'sentry-anr'; + function log(message: string): void { logger.log(`[ANR child process] ${message}`); } - process.title = 'sentry-anr'; - log('Started'); + let session: Session | undefined; + + function sendAnrEvent(frames?: StackFrame[]): void { + if (session) { + log('Sending abnormal session'); + updateSession(session, { status: 'abnormal', abnormal_mechanism: 'anr_foreground' }); + getCurrentHub().getClient()?.sendSession(session); + + try { + // Notify the main process that the session has ended so the session can be cleared from the scope + process.send?.('session-ended'); + } catch (_) { + // ignore + } + } + + captureEvent(createAnrEvent(options.anrThreshold, frames)); + + void flush(3000).then(() => { + // We only capture one event to avoid spamming users with errors + process.exit(); + }); + } addGlobalEventProcessor(event => { // Strip sdkProcessingMetadata from all child process events to remove trace info @@ -179,7 +209,7 @@ function handleChildProcess(options: Options): void { debuggerPause = captureStackTrace(process.env.SENTRY_INSPECT_URL, frames => { log('Capturing event with stack frames'); - sendEvent(options.anrThreshold, frames); + sendAnrEvent(frames); }); } @@ -192,13 +222,16 @@ function handleChildProcess(options: Options): void { pauseAndCapture(); } else { log('Capturing event'); - sendEvent(options.anrThreshold); + sendAnrEvent(); } } const { poll } = watchdogTimer(createHrTimer, options.pollInterval, options.anrThreshold, watchdogTimeout); - process.on('message', () => { + process.on('message', (msg: { session: Session | undefined }) => { + if (msg.session) { + session = makeSession(msg.session); + } poll(); }); } diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index 78b7a5b39654..5bc49b9a7733 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -21,7 +21,7 @@ export interface Session { errors: number; user?: User | null; ignoreDuration: boolean; - + abnormal_mechanism?: string; /** * Overrides default JSON serialization of the Session because * the Sentry servers expect a slightly different schema of a session @@ -76,6 +76,7 @@ export interface SerializedSession { duration?: number; status: SessionStatus; errors: number; + abnormal_mechanism?: string; attrs?: { release?: string; environment?: string; From 595ee3c58efbe3447639ae8d411cbb84723cb7aa Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 18 Oct 2023 18:08:54 +0200 Subject: [PATCH 06/38] chore(repo): Change SDK selection order in bug issue template (#9288) --- .github/ISSUE_TEMPLATE/bug.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index c7e01af332a2..044efc2b9696 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -30,8 +30,8 @@ body: If you're using the CDN bundles, please specify the exact bundle (e.g. `bundle.tracing.min.js`) in your SDK setup. options: - - '@sentry/astro' - '@sentry/browser' + - '@sentry/astro' - '@sentry/angular' - '@sentry/angular-ivy' - '@sentry/bun' From da339b4ecca8e1377c3765fa27f6386ba38c437c Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 18 Oct 2023 20:07:15 +0200 Subject: [PATCH 07/38] feat(node): Remove `lru_map` dependency (#9300) --- packages/deno/package.json | 4 +- packages/deno/rollup.config.js | 2 - .../deno/src/integrations/contextlines.ts | 3 +- packages/node/package.json | 1 - .../node/src/integrations/contextlines.ts | 3 +- packages/node/src/integrations/http.ts | 2 +- .../node/src/integrations/localvariables.ts | 7 +-- .../node/src/integrations/undici/index.ts | 2 +- .../test/integrations/localvariables.test.ts | 14 +---- packages/utils/src/index.ts | 1 + packages/utils/src/lru.ts | 60 +++++++++++++++++++ packages/utils/test/lru.test.ts | 41 +++++++++++++ yarn.lock | 17 ------ 13 files changed, 113 insertions(+), 44 deletions(-) create mode 100644 packages/utils/src/lru.ts create mode 100644 packages/utils/test/lru.test.ts diff --git a/packages/deno/package.json b/packages/deno/package.json index 75f8526efca4..4e875141a229 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -21,11 +21,9 @@ "@sentry/browser": "7.74.1", "@sentry/core": "7.74.1", "@sentry/types": "7.74.1", - "@sentry/utils": "7.74.1", - "lru_map": "^0.3.3" + "@sentry/utils": "7.74.1" }, "devDependencies": { - "@rollup/plugin-commonjs": "^25.0.5", "@rollup/plugin-typescript": "^11.1.5", "@types/node": "20.8.2", "rollup-plugin-dts": "^6.1.0" diff --git a/packages/deno/rollup.config.js b/packages/deno/rollup.config.js index 236501153f8b..d79b77478053 100644 --- a/packages/deno/rollup.config.js +++ b/packages/deno/rollup.config.js @@ -1,6 +1,5 @@ // @ts-check import nodeResolve from '@rollup/plugin-node-resolve'; -import commonjs from '@rollup/plugin-commonjs'; import sucrase from '@rollup/plugin-sucrase'; import { defineConfig } from 'rollup'; @@ -21,7 +20,6 @@ export default defineConfig({ nodeResolve({ extensions: ['.mjs', '.js', '.json', '.node', '.ts', '.tsx'], }), - commonjs(), sucrase({ transforms: ['typescript'] }), ], }); diff --git a/packages/deno/src/integrations/contextlines.ts b/packages/deno/src/integrations/contextlines.ts index 47cd3a09218d..ab6db13f5ef2 100644 --- a/packages/deno/src/integrations/contextlines.ts +++ b/packages/deno/src/integrations/contextlines.ts @@ -1,6 +1,5 @@ import type { Event, EventProcessor, Integration, StackFrame } from '@sentry/types'; -import { addContextToFrame } from '@sentry/utils'; -import { LRUMap } from 'lru_map'; +import { addContextToFrame, LRUMap } from '@sentry/utils'; const FILE_CONTENT_CACHE = new LRUMap(100); const DEFAULT_LINES_OF_CONTEXT = 7; diff --git a/packages/node/package.json b/packages/node/package.json index 4e2a32d8c858..9d99aecd0b5b 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -29,7 +29,6 @@ "@sentry/utils": "7.74.1", "cookie": "^0.5.0", "https-proxy-agent": "^5.0.0", - "lru_map": "^0.3.3", "tslib": "^2.4.1 || ^1.9.3" }, "devDependencies": { diff --git a/packages/node/src/integrations/contextlines.ts b/packages/node/src/integrations/contextlines.ts index d1c2056c0c66..f47e31eb268a 100644 --- a/packages/node/src/integrations/contextlines.ts +++ b/packages/node/src/integrations/contextlines.ts @@ -1,7 +1,6 @@ import type { Event, EventProcessor, Hub, Integration, StackFrame } from '@sentry/types'; -import { addContextToFrame } from '@sentry/utils'; +import { addContextToFrame, LRUMap } from '@sentry/utils'; import { readFile } from 'fs'; -import { LRUMap } from 'lru_map'; const FILE_CONTENT_CACHE = new LRUMap(100); const DEFAULT_LINES_OF_CONTEXT = 7; diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 1f4b5c9c9224..4fd2e702bb44 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -12,11 +12,11 @@ import { fill, generateSentryTraceHeader, logger, + LRUMap, stringMatchesSomePattern, } from '@sentry/utils'; import type * as http from 'http'; import type * as https from 'https'; -import { LRUMap } from 'lru_map'; import type { NodeClient } from '../client'; import { NODE_VERSION } from '../nodeVersion'; diff --git a/packages/node/src/integrations/localvariables.ts b/packages/node/src/integrations/localvariables.ts index 39f250c23778..5ac70db4a839 100644 --- a/packages/node/src/integrations/localvariables.ts +++ b/packages/node/src/integrations/localvariables.ts @@ -1,8 +1,7 @@ /* eslint-disable max-lines */ import type { Event, EventProcessor, Exception, Hub, Integration, StackFrame, StackParser } from '@sentry/types'; -import { logger } from '@sentry/utils'; +import { logger, LRUMap } from '@sentry/utils'; import type { Debugger, InspectorNotification, Runtime, Session } from 'inspector'; -import { LRUMap } from 'lru_map'; import { NODE_VERSION } from '../nodeVersion'; import type { NodeClientOptions } from '../types'; @@ -470,8 +469,8 @@ export class LocalVariables implements Integration { } // Check if we have local variables for an exception that matches the hash - // delete is identical to get but also removes the entry from the cache - const cachedFrames = this._cachedFrames.delete(hash); + // remove is identical to get but also removes the entry from the cache + const cachedFrames = this._cachedFrames.remove(hash); if (cachedFrames === undefined) { return; diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node/src/integrations/undici/index.ts index 25888780a30c..ff08d1df0f65 100644 --- a/packages/node/src/integrations/undici/index.ts +++ b/packages/node/src/integrations/undici/index.ts @@ -5,10 +5,10 @@ import { dynamicSamplingContextToSentryBaggageHeader, generateSentryTraceHeader, getSanitizedUrlString, + LRUMap, parseUrl, stringMatchesSomePattern, } from '@sentry/utils'; -import { LRUMap } from 'lru_map'; import type { NodeClient } from '../../client'; import { NODE_VERSION } from '../../nodeVersion'; diff --git a/packages/node/test/integrations/localvariables.test.ts b/packages/node/test/integrations/localvariables.test.ts index 55e179c879e6..f48666658e79 100644 --- a/packages/node/test/integrations/localvariables.test.ts +++ b/packages/node/test/integrations/localvariables.test.ts @@ -1,6 +1,6 @@ import type { ClientOptions, EventProcessor } from '@sentry/types'; +import type { LRUMap } from '@sentry/utils'; import type { Debugger, InspectorNotification } from 'inspector'; -import type { LRUMap } from 'lru_map'; import { defaultStackParser } from '../../src'; import type { DebugSession, FrameVariables } from '../../src/integrations/localvariables'; @@ -178,11 +178,7 @@ describeIf((NODE_VERSION.major || 0) >= 18)('LocalVariables', () => { expect((localVariables as unknown as LocalVariablesPrivate)._cachedFrames.size).toBe(1); - let frames: FrameVariables[] | undefined; - - (localVariables as unknown as LocalVariablesPrivate)._cachedFrames.forEach(f => { - frames = f; - }); + const frames: FrameVariables[] = (localVariables as unknown as LocalVariablesPrivate)._cachedFrames.values()[0]; expect(frames).toBeDefined(); @@ -274,11 +270,7 @@ describeIf((NODE_VERSION.major || 0) >= 18)('LocalVariables', () => { expect((localVariables as unknown as LocalVariablesPrivate)._cachedFrames.size).toBe(1); - let frames: FrameVariables[] | undefined; - - (localVariables as unknown as LocalVariablesPrivate)._cachedFrames.forEach(f => { - frames = f; - }); + const frames: FrameVariables[] = (localVariables as unknown as LocalVariablesPrivate)._cachedFrames.values()[0]; expect(frames).toBeDefined(); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 81f4d947cd0d..5bb54c52b9cd 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -32,3 +32,4 @@ export * from './userIntegrations'; export * from './cache'; export * from './eventbuilder'; export * from './anr'; +export * from './lru'; diff --git a/packages/utils/src/lru.ts b/packages/utils/src/lru.ts new file mode 100644 index 000000000000..2a3b7bfc8ac0 --- /dev/null +++ b/packages/utils/src/lru.ts @@ -0,0 +1,60 @@ +/** A simple Least Recently Used map */ +export class LRUMap { + private readonly _cache: Map; + + public constructor(private readonly _maxSize: number) { + this._cache = new Map(); + } + + /** Get the current size of the cache */ + public get size(): number { + return this._cache.size; + } + + /** Get an entry or undefined if it was not in the cache. Re-inserts to update the recently used order */ + public get(key: K): V | undefined { + const value = this._cache.get(key); + if (value === undefined) { + return undefined; + } + // Remove and re-insert to update the order + this._cache.delete(key); + this._cache.set(key, value); + return value; + } + + /** Insert an entry and evict an older entry if we've reached maxSize */ + public set(key: K, value: V): void { + if (this._cache.size >= this._maxSize) { + // keys() returns an iterator in insertion order so keys().next() gives us the oldest key + this._cache.delete(this._cache.keys().next().value); + } + this._cache.set(key, value); + } + + /** Remove an entry and return the entry if it was in the cache */ + public remove(key: K): V | undefined { + const value = this._cache.get(key); + if (value) { + this._cache.delete(key); + } + return value; + } + + /** Clear all entries */ + public clear(): void { + this._cache.clear(); + } + + /** Get all the keys */ + public keys(): Array { + return Array.from(this._cache.keys()); + } + + /** Get all the values */ + public values(): Array { + const values: V[] = []; + this._cache.forEach(value => values.push(value)); + return values; + } +} diff --git a/packages/utils/test/lru.test.ts b/packages/utils/test/lru.test.ts new file mode 100644 index 000000000000..15f638e9cfa1 --- /dev/null +++ b/packages/utils/test/lru.test.ts @@ -0,0 +1,41 @@ +import { LRUMap } from '../src/lru'; + +describe('LRUMap', () => { + test('evicts older entries when reaching max size', () => { + const map = new LRUMap(3); + map.set('a', '1'); + map.set('b', '2'); + map.set('c', '3'); + map.set('d', '4'); + map.set('e', '5'); + + expect(map.keys()).toEqual(['c', 'd', 'e']); + }); + + test('updates last used when calling get', () => { + const map = new LRUMap(3); + map.set('a', '1'); + map.set('b', '2'); + map.set('c', '3'); + + map.get('a'); + + map.set('d', '4'); + map.set('e', '5'); + + expect(map.keys()).toEqual(['a', 'd', 'e']); + }); + + test('removes and returns entry', () => { + const map = new LRUMap(3); + map.set('a', '1'); + map.set('b', '2'); + map.set('c', '3'); + map.set('d', '4'); + map.set('e', '5'); + + expect(map.remove('c')).toEqual('3'); + + expect(map.keys()).toEqual(['d', 'e']); + }); +}); diff --git a/yarn.lock b/yarn.lock index e4e1855b1d18..5d2abce2c168 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4800,18 +4800,6 @@ magic-string "^0.25.7" resolve "^1.17.0" -"@rollup/plugin-commonjs@^25.0.5": - version "25.0.5" - resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.5.tgz#0bac8f985a5de151b4b09338847f8c7f20a28a29" - integrity sha512-xY8r/A9oisSeSuLCTfhssyDjo9Vp/eDiRLXkg1MXCcEEgEjPmLU+ZyDB20OOD0NlyDa/8SGbK5uIggF5XTx77w== - dependencies: - "@rollup/pluginutils" "^5.0.1" - commondir "^1.0.1" - estree-walker "^2.0.2" - glob "^8.0.3" - is-reference "1.2.1" - magic-string "^0.27.0" - "@rollup/plugin-json@^4.0.0", "@rollup/plugin-json@^4.1.0": version "4.1.0" resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-4.1.0.tgz#54e09867ae6963c593844d8bd7a9c718294496f3" @@ -19997,11 +19985,6 @@ lru-cache@^9.0.0: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.0.tgz#b9e2a6a72a129d81ab317202d93c7691df727e61" integrity sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw== -lru_map@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" - integrity sha1-tcg1G5Rky9dQM1p5ZQoOwOVhGN0= - lunr@^2.3.8: version "2.3.9" resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" From 753f82bb697ab1f64e0ba6c61362dba6790fa067 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 18 Oct 2023 18:44:55 -0400 Subject: [PATCH 08/38] feat: Remove tslib (#9299) --- .vscode/launch.json | 7 +------ package.json | 1 - packages/browser/package.json | 3 +-- packages/core/package.json | 3 +-- .../e2e-tests/test-applications/sveltekit/package.json | 1 - packages/feedback/package.json | 3 +-- packages/hub/package.json | 3 +-- packages/integrations/package.json | 3 +-- packages/nextjs/package.json | 3 +-- packages/node/package.json | 3 +-- packages/react/package.json | 3 +-- packages/remix/package.json | 1 - packages/replay-worker/package.json | 3 +-- packages/replay/package.json | 3 +-- packages/serverless/package.json | 3 +-- packages/svelte/package.json | 3 +-- packages/tracing-internal/package.json | 3 +-- packages/utils/package.json | 3 +-- packages/vercel-edge/package.json | 3 +-- packages/vue/package.json | 3 +-- packages/wasm/package.json | 3 +-- yarn.lock | 5 ----- 22 files changed, 18 insertions(+), 48 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 2fd396c8ddbf..409d7264a7e0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -129,12 +129,7 @@ "${fileBasename}" ], - "skipFiles": [ - "/**", - // this prevents us from landing in a neverending cycle of TS async-polyfill functions as we're stepping through - // our code - "${workspaceFolder}/node_modules/tslib/**/*" - ], + "skipFiles": ["/**"], "sourceMaps": true, // this controls which files are sourcemapped "outFiles": [ diff --git a/package.json b/package.json index 364c97a53a75..2784d0c6be9d 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,6 @@ "size-limit": "~9.0.0", "ts-jest": "^27.1.4", "ts-node": "10.9.1", - "tslib": "2.4.1", "typedoc": "^0.18.0", "typescript": "4.9.5", "vitest": "^0.29.2", diff --git a/packages/browser/package.json b/packages/browser/package.json index df12646f0126..d02a14239955 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -27,8 +27,7 @@ "@sentry/core": "7.74.1", "@sentry/replay": "7.74.1", "@sentry/types": "7.74.1", - "@sentry/utils": "7.74.1", - "tslib": "^2.4.1 || ^1.9.3" + "@sentry/utils": "7.74.1" }, "devDependencies": { "@sentry-internal/integration-shims": "7.74.1", diff --git a/packages/core/package.json b/packages/core/package.json index 43c0420cf820..8f803c731e32 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -24,8 +24,7 @@ }, "dependencies": { "@sentry/types": "7.74.1", - "@sentry/utils": "7.74.1", - "tslib": "^2.4.1 || ^1.9.3" + "@sentry/utils": "7.74.1" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/e2e-tests/test-applications/sveltekit/package.json b/packages/e2e-tests/test-applications/sveltekit/package.json index 5adf85d8e7d8..ad6bf6456843 100644 --- a/packages/e2e-tests/test-applications/sveltekit/package.json +++ b/packages/e2e-tests/test-applications/sveltekit/package.json @@ -27,7 +27,6 @@ "svelte": "^3.54.0", "svelte-check": "^3.0.1", "ts-node": "10.9.1", - "tslib": "2.4.1", "typescript": "^5.0.0", "vite": "^4.2.0", "wait-port": "1.0.4" diff --git a/packages/feedback/package.json b/packages/feedback/package.json index 1c118abaecfc..b1b822dc461b 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -25,8 +25,7 @@ "dependencies": { "@sentry/core": "7.70.0", "@sentry/types": "7.70.0", - "@sentry/utils": "7.70.0", - "tslib": "^2.4.1 || ^1.9.3" + "@sentry/utils": "7.70.0" }, "scripts": { "build": "run-p build:transpile build:types build:bundle", diff --git a/packages/hub/package.json b/packages/hub/package.json index d4d8d8084040..4d6df0cd3f57 100644 --- a/packages/hub/package.json +++ b/packages/hub/package.json @@ -25,8 +25,7 @@ "dependencies": { "@sentry/core": "7.74.1", "@sentry/types": "7.74.1", - "@sentry/utils": "7.74.1", - "tslib": "^2.4.1 || ^1.9.3" + "@sentry/utils": "7.74.1" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/integrations/package.json b/packages/integrations/package.json index 4029089a64b9..f5e9911b363f 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -26,8 +26,7 @@ "@sentry/core": "7.74.1", "@sentry/types": "7.74.1", "@sentry/utils": "7.74.1", - "localforage": "^1.8.1", - "tslib": "^2.4.1 || ^1.9.3" + "localforage": "^1.8.1" }, "devDependencies": { "@sentry/browser": "7.74.1", diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 99db628d82d6..05aaa3f47cca 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -36,8 +36,7 @@ "chalk": "3.0.0", "resolve": "1.22.8", "rollup": "2.78.0", - "stacktrace-parser": "^0.1.10", - "tslib": "^2.4.1 || ^1.9.3" + "stacktrace-parser": "^0.1.10" }, "devDependencies": { "@types/resolve": "1.20.3", diff --git a/packages/node/package.json b/packages/node/package.json index 9d99aecd0b5b..6f0da4a58815 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -28,8 +28,7 @@ "@sentry/types": "7.74.1", "@sentry/utils": "7.74.1", "cookie": "^0.5.0", - "https-proxy-agent": "^5.0.0", - "tslib": "^2.4.1 || ^1.9.3" + "https-proxy-agent": "^5.0.0" }, "devDependencies": { "@types/cookie": "0.5.2", diff --git a/packages/react/package.json b/packages/react/package.json index 17fb62983835..cf33891c6585 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -26,8 +26,7 @@ "@sentry/browser": "7.74.1", "@sentry/types": "7.74.1", "@sentry/utils": "7.74.1", - "hoist-non-react-statics": "^3.3.2", - "tslib": "^2.4.1 || ^1.9.3" + "hoist-non-react-statics": "^3.3.2" }, "peerDependencies": { "react": "15.x || 16.x || 17.x || 18.x" diff --git a/packages/remix/package.json b/packages/remix/package.json index b6a3789619eb..3d238a2bedfe 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -34,7 +34,6 @@ "@sentry/types": "7.74.1", "@sentry/utils": "7.74.1", "glob": "^10.3.4", - "tslib": "^2.4.1 || ^1.9.3", "yargs": "^17.6.0" }, "devDependencies": { diff --git a/packages/replay-worker/package.json b/packages/replay-worker/package.json index a07ffb610bc1..ef42a28e6160 100644 --- a/packages/replay-worker/package.json +++ b/packages/replay-worker/package.json @@ -46,8 +46,7 @@ }, "homepage": "https://docs.sentry.io/platforms/javascript/session-replay/", "devDependencies": { - "@types/pako": "^2.0.0", - "tslib": "^2.4.1 || ^1.9.3" + "@types/pako": "^2.0.0" }, "dependencies": { "pako": "^2.1.0" diff --git a/packages/replay/package.json b/packages/replay/package.json index 0b355a26ef0e..c91d06510008 100644 --- a/packages/replay/package.json +++ b/packages/replay/package.json @@ -54,8 +54,7 @@ "@sentry-internal/replay-worker": "7.74.1", "@sentry-internal/rrweb": "2.0.1", "@sentry-internal/rrweb-snapshot": "2.0.1", - "jsdom-worker": "^0.2.1", - "tslib": "^2.4.1 || ^1.9.3" + "jsdom-worker": "^0.2.1" }, "dependencies": { "@sentry/core": "7.74.1", diff --git a/packages/serverless/package.json b/packages/serverless/package.json index d63c6b96f641..5259a838c282 100644 --- a/packages/serverless/package.json +++ b/packages/serverless/package.json @@ -28,8 +28,7 @@ "@sentry/types": "7.74.1", "@sentry/utils": "7.74.1", "@types/aws-lambda": "^8.10.62", - "@types/express": "^4.17.14", - "tslib": "^2.4.1 || ^1.9.3" + "@types/express": "^4.17.14" }, "devDependencies": { "@google-cloud/bigquery": "^5.3.0", diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 3fb245b3c4dd..ff7515b6bf50 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -26,8 +26,7 @@ "@sentry/browser": "7.74.1", "@sentry/types": "7.74.1", "@sentry/utils": "7.74.1", - "magic-string": "^0.30.0", - "tslib": "^2.4.1 || ^1.9.3" + "magic-string": "^0.30.0" }, "peerDependencies": { "svelte": "3.x || 4.x" diff --git a/packages/tracing-internal/package.json b/packages/tracing-internal/package.json index ec4e8b6278c9..acbab9607598 100644 --- a/packages/tracing-internal/package.json +++ b/packages/tracing-internal/package.json @@ -25,8 +25,7 @@ "dependencies": { "@sentry/core": "7.74.1", "@sentry/types": "7.74.1", - "@sentry/utils": "7.74.1", - "tslib": "^2.4.1 || ^1.9.3" + "@sentry/utils": "7.74.1" }, "devDependencies": { "@types/express": "^4.17.14" diff --git a/packages/utils/package.json b/packages/utils/package.json index 05a4e51da48d..26a03c03f78a 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -23,8 +23,7 @@ "access": "public" }, "dependencies": { - "@sentry/types": "7.74.1", - "tslib": "^2.4.1 || ^1.9.3" + "@sentry/types": "7.74.1" }, "devDependencies": { "@types/array.prototype.flat": "^1.2.1", diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json index 3267a87bfd02..f8081015dcb3 100644 --- a/packages/vercel-edge/package.json +++ b/packages/vercel-edge/package.json @@ -25,8 +25,7 @@ "dependencies": { "@sentry/core": "7.74.1", "@sentry/types": "7.74.1", - "@sentry/utils": "7.74.1", - "tslib": "^2.4.1 || ^1.9.3" + "@sentry/utils": "7.74.1" }, "devDependencies": { "@edge-runtime/jest-environment": "2.2.3", diff --git a/packages/vue/package.json b/packages/vue/package.json index 2e766d4b1357..ce1b5057c38e 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -26,8 +26,7 @@ "@sentry/browser": "7.74.1", "@sentry/core": "7.74.1", "@sentry/types": "7.74.1", - "@sentry/utils": "7.74.1", - "tslib": "^2.4.1 || ^1.9.3" + "@sentry/utils": "7.74.1" }, "peerDependencies": { "vue": "2.x || 3.x" diff --git a/packages/wasm/package.json b/packages/wasm/package.json index 9cf2c5953f07..e7e9f309f1c5 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -25,8 +25,7 @@ "dependencies": { "@sentry/browser": "7.74.1", "@sentry/types": "7.74.1", - "@sentry/utils": "7.74.1", - "tslib": "^2.4.1 || ^1.9.3" + "@sentry/utils": "7.74.1" }, "scripts": { "build": "run-p build:transpile build:bundle build:types", diff --git a/yarn.lock b/yarn.lock index 5d2abce2c168..25c03695adc2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29387,11 +29387,6 @@ tslib@2.3.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== -tslib@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" - integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== - tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" From 905a3a1020c69c6ba2ce1f7fd877d2331ef34571 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 19 Oct 2023 09:23:13 +0200 Subject: [PATCH 09/38] test(deno): Refer to local files in tests (#9297) --- .../deno/test/__snapshots__/mod.test.ts.snap | 4 +-- packages/deno/test/mod.test.ts | 7 +++-- packages/deno/test/normalize.ts | 27 +++++++++---------- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/packages/deno/test/__snapshots__/mod.test.ts.snap b/packages/deno/test/__snapshots__/mod.test.ts.snap index 749d3ce9d238..aa541d9679b9 100644 --- a/packages/deno/test/__snapshots__/mod.test.ts.snap +++ b/packages/deno/test/__snapshots__/mod.test.ts.snap @@ -82,7 +82,7 @@ snapshot[`captureException 1`] = ` filename: "app:///test/mod.test.ts", function: "", in_app: true, - lineno: 43, + lineno: 42, post_context: [ "", " await delay(200);", @@ -108,7 +108,7 @@ snapshot[`captureException 1`] = ` filename: "app:///test/mod.test.ts", function: "something", in_app: true, - lineno: 40, + lineno: 39, post_context: [ " }", "", diff --git a/packages/deno/test/mod.test.ts b/packages/deno/test/mod.test.ts index 1724125415cd..7f57616816d5 100644 --- a/packages/deno/test/mod.test.ts +++ b/packages/deno/test/mod.test.ts @@ -1,13 +1,12 @@ import { assertEquals } from 'https://deno.land/std@0.202.0/assert/assert_equals.ts'; import { assertSnapshot } from 'https://deno.land/std@0.202.0/testing/snapshot.ts'; -import type { Event, Integration } from 'npm:@sentry/types'; -import { createStackParser, nodeStackLineParser } from 'npm:@sentry/utils'; +import { createStackParser, nodeStackLineParser } from '../../utils/build/esm/index.js'; import { defaultIntegrations, DenoClient, Hub, Scope } from '../build/index.js'; import { getNormalizedEvent } from './normalize.ts'; import { makeTestTransport } from './transport.ts'; -function getTestClient(callback: (event?: Event) => void, integrations: Integration[] = []): [Hub, DenoClient] { +function getTestClient(callback: (event?: Event) => void, integrations: any[] = []): [Hub, DenoClient] { const client = new DenoClient({ dsn: 'https://233a45e5efe34c47a3536797ce15dafa@nothing.here/5650507', debug: true, @@ -15,7 +14,7 @@ function getTestClient(callback: (event?: Event) => void, integrations: Integrat stackParser: createStackParser(nodeStackLineParser()), transport: makeTestTransport(envelope => { callback(getNormalizedEvent(envelope)); - }), + }) as any, }); const scope = new Scope(); diff --git a/packages/deno/test/normalize.ts b/packages/deno/test/normalize.ts index b36cf4ac52a9..ad7081c52efe 100644 --- a/packages/deno/test/normalize.ts +++ b/packages/deno/test/normalize.ts @@ -1,21 +1,20 @@ /* eslint-disable complexity */ -import type { Envelope, Event, Session, Transaction } from 'npm:@sentry/types'; -import { forEachEnvelopeItem } from 'npm:@sentry/utils'; +import { forEachEnvelopeItem } from '../../utils/build/esm/index.js'; -type EventOrSession = Event | Transaction | Session; +type EventOrSession = any; -export function getNormalizedEvent(envelope: Envelope): Event | undefined { - let event: Event | undefined; +export function getNormalizedEvent(envelope: any): any | undefined { + let event: any | undefined; - forEachEnvelopeItem(envelope, item => { + forEachEnvelopeItem(envelope, (item: any) => { const [headers, body] = item; if (headers.type === 'event') { - event = body as Event; + event = body; } }); - return normalize(event) as Event | undefined; + return normalize(event) as any | undefined; } export function normalize(event: EventOrSession | undefined): EventOrSession | undefined { @@ -24,14 +23,14 @@ export function normalize(event: EventOrSession | undefined): EventOrSession | u } if (eventIsSession(event)) { - return normalizeSession(event as Session); + return normalizeSession(event); } else { - return normalizeEvent(event as Event); + return normalizeEvent(event); } } export function eventIsSession(data: EventOrSession): boolean { - return !!(data as Session)?.sid; + return !!data?.sid; } /** @@ -40,7 +39,7 @@ export function eventIsSession(data: EventOrSession): boolean { * All properties that are timestamps, versions, ids or variables that may vary * by platform are replaced with placeholder strings */ -function normalizeSession(session: Session): Session { +function normalizeSession(session: any): any { if (session.sid) { session.sid = '{{id}}'; } @@ -66,7 +65,7 @@ function normalizeSession(session: Session): Session { * All properties that are timestamps, versions, ids or variables that may vary * by platform are replaced with placeholder strings */ -function normalizeEvent(event: Event): Event { +function normalizeEvent(event: any): any { if (event.sdk?.version) { event.sdk.version = '{{version}}'; } @@ -157,7 +156,7 @@ function normalizeEvent(event: Event): Event { if (event.exception?.values?.[0].stacktrace?.frames) { // Exlcude Deno frames since these may change between versions event.exception.values[0].stacktrace.frames = event.exception.values[0].stacktrace.frames.filter( - frame => !frame.filename?.includes('deno:'), + (frame: any) => !frame.filename?.includes('deno:'), ); } From dc68c096964e362677ccb1343bd4094206193ea1 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 19 Oct 2023 09:26:07 +0200 Subject: [PATCH 10/38] feat(opentelemetry): Add new `@sentry/opentelemetry` package (#9238) This extracts the code from `node-experimental` in a more reusable `@sentry/opentelemetry` package. This package has no direct (otel) dependencies, but only peer dependencies. This means it can be used by anybody with an existing OTEL setup without messing with their dependencies etc. In turn, `node-experimental` uses this package to implement a concrete and opinionated implementation of this for node. This new package should also be usable for non-node OTEL implementations, and concisously does not include any node specific stuff. --- .craft.yml | 4 + docs/new-sdk-release-checklist.md | 6 +- package.json | 1 + .../e2e-tests/verdaccio-config/config.yaml | 6 + packages/node-experimental/README.md | 2 +- packages/node-experimental/package.json | 36 +- packages/node-experimental/src/constants.ts | 21 - packages/node-experimental/src/index.ts | 5 +- .../src/integrations/http.ts | 13 +- .../src/integrations/node-fetch.ts | 12 +- .../src/opentelemetry/contextManager.ts | 37 -- .../src/opentelemetry/spanProcessor.ts | 99 ---- packages/node-experimental/src/sdk/client.ts | 74 +-- packages/node-experimental/src/sdk/init.ts | 12 +- .../node-experimental/src/sdk/initOtel.ts | 22 +- .../src/sdk/spanProcessor.ts | 39 ++ packages/node-experimental/src/types.ts | 18 +- .../src/utils/addOriginToSpan.ts | 5 +- .../test/integration/breadcrumbs.test.ts | 18 +- .../test/integration/scope.test.ts | 29 +- .../test/integration/transactions.test.ts | 11 +- packages/opentelemetry-node/package.json | 8 +- .../test/spanprocessor.test.ts | 4 +- packages/opentelemetry/.eslintrc.js | 9 + packages/opentelemetry/LICENSE | 14 + packages/opentelemetry/README.md | 98 ++++ packages/opentelemetry/jest.config.js | 1 + packages/opentelemetry/package.json | 73 +++ packages/opentelemetry/rollup.npm.config.js | 3 + .../src/asyncContextStrategy.ts} | 7 +- packages/opentelemetry/src/constants.ts | 10 + packages/opentelemetry/src/contextManager.ts | 57 ++ packages/opentelemetry/src/custom/client.ts | 81 +++ .../sdk => opentelemetry/src/custom}/hub.ts | 28 +- .../src/custom}/hubextensions.ts | 2 +- .../sdk => opentelemetry/src/custom}/scope.ts | 39 +- .../src/custom}/transaction.ts | 4 +- packages/opentelemetry/src/index.ts | 47 ++ .../src}/propagator.ts | 15 +- .../src}/sampler.ts | 19 +- .../opentelemetry/src/semanticAttributes.ts | 16 + .../src}/setupEventContextTrace.ts | 4 +- .../src}/spanExporter.ts | 74 +-- packages/opentelemetry/src/spanProcessor.ts | 86 +++ .../src/sdk => opentelemetry/src}/trace.ts | 65 +-- packages/opentelemetry/src/types.ts | 29 + .../src/utils/addOriginToSpan.ts | 9 + .../utils/captureExceptionForTimedEvent.ts | 55 ++ .../opentelemetry/src/utils/contextData.ts | 36 ++ .../src/utils/convertOtelTimeToSeconds.ts | 0 .../src/utils/getActiveSpan.ts | 2 +- .../src/utils/getRequestSpanData.ts | 0 .../src/utils/getSpanKind.ts | 4 +- .../src/utils/groupSpansWithParents.ts | 2 +- .../src/utils/isSentryRequest.ts | 26 + packages/opentelemetry/src/utils/mapStatus.ts | 74 +++ .../src/utils/parseSpanDescription.ts | 166 ++++++ .../src/utils}/spanData.ts | 0 .../src/utils/spanTypes.ts | 32 +- .../test/asyncContextStrategy.test.ts} | 21 +- .../opentelemetry/test/custom/client.test.ts | 19 + .../test/custom}/hub.test.ts | 22 +- .../test/custom}/hubextensions.test.ts | 11 +- .../test/custom}/scope.test.ts | 90 ++- .../test/custom}/transaction.test.ts | 31 +- .../opentelemetry/test/helpers/TestClient.ts | 49 ++ .../opentelemetry/test/helpers/createSpan.ts | 30 + .../opentelemetry/test/helpers/initOtel.ts | 72 +++ .../opentelemetry/test/helpers/mockSdkInit.ts | 68 +++ .../test/integration/breadcrumbs.test.ts | 361 ++++++++++++ .../test/integration/otelTimedEvents.test.ts | 14 +- .../test/integration/scope.test.ts | 238 ++++++++ .../test/integration/transactions.test.ts | 536 ++++++++++++++++++ .../test}/propagator.test.ts | 40 +- .../sdk => opentelemetry/test}/trace.test.ts | 338 ++++++----- .../captureExceptionForTimedEvent.test.ts | 147 +++++ .../utils/convertOtelTimeToSeconds.test.ts | 0 .../test/utils/getActiveSpan.test.ts | 11 +- .../test/utils/getRequestSpanData.test.ts | 0 .../test/utils/getSpanKind.test.ts | 11 + .../test/utils/groupSpansWithParents.test.ts | 0 .../test/utils/mapStatus.test.ts | 83 +++ .../test/utils/parseSpanDescription.test.ts | 340 +++++++++++ .../test/utils/setupEventContextTrace.test.ts | 25 +- .../test/utils/spanTypes.test.ts | 27 +- packages/opentelemetry/tsconfig.json | 9 + packages/opentelemetry/tsconfig.test.json | 12 + packages/opentelemetry/tsconfig.types.json | 10 + scripts/node-unit-tests.ts | 3 + yarn.lock | 293 +++++----- 90 files changed, 3618 insertions(+), 962 deletions(-) delete mode 100644 packages/node-experimental/src/constants.ts delete mode 100644 packages/node-experimental/src/opentelemetry/contextManager.ts delete mode 100644 packages/node-experimental/src/opentelemetry/spanProcessor.ts create mode 100644 packages/node-experimental/src/sdk/spanProcessor.ts create mode 100644 packages/opentelemetry/.eslintrc.js create mode 100644 packages/opentelemetry/LICENSE create mode 100644 packages/opentelemetry/README.md create mode 100644 packages/opentelemetry/jest.config.js create mode 100644 packages/opentelemetry/package.json create mode 100644 packages/opentelemetry/rollup.npm.config.js rename packages/{node-experimental/src/sdk/otelAsyncContextStrategy.ts => opentelemetry/src/asyncContextStrategy.ts} (82%) create mode 100644 packages/opentelemetry/src/constants.ts create mode 100644 packages/opentelemetry/src/contextManager.ts create mode 100644 packages/opentelemetry/src/custom/client.ts rename packages/{node-experimental/src/sdk => opentelemetry/src/custom}/hub.ts (83%) rename packages/{node-experimental/src/sdk => opentelemetry/src/custom}/hubextensions.ts (90%) rename packages/{node-experimental/src/sdk => opentelemetry/src/custom}/scope.ts (77%) rename packages/{node-experimental/src/sdk => opentelemetry/src/custom}/transaction.ts (90%) create mode 100644 packages/opentelemetry/src/index.ts rename packages/{node-experimental/src/opentelemetry => opentelemetry/src}/propagator.ts (90%) rename packages/{node-experimental/src/opentelemetry => opentelemetry/src}/sampler.ts (90%) create mode 100644 packages/opentelemetry/src/semanticAttributes.ts rename packages/{node-experimental/src/utils => opentelemetry/src}/setupEventContextTrace.ts (86%) rename packages/{node-experimental/src/opentelemetry => opentelemetry/src}/spanExporter.ts (79%) create mode 100644 packages/opentelemetry/src/spanProcessor.ts rename packages/{node-experimental/src/sdk => opentelemetry/src}/trace.ts (50%) create mode 100644 packages/opentelemetry/src/types.ts create mode 100644 packages/opentelemetry/src/utils/addOriginToSpan.ts create mode 100644 packages/opentelemetry/src/utils/captureExceptionForTimedEvent.ts create mode 100644 packages/opentelemetry/src/utils/contextData.ts rename packages/{node-experimental => opentelemetry}/src/utils/convertOtelTimeToSeconds.ts (100%) rename packages/{node-experimental => opentelemetry}/src/utils/getActiveSpan.ts (89%) rename packages/{node-experimental => opentelemetry}/src/utils/getRequestSpanData.ts (100%) rename packages/{node-experimental => opentelemetry}/src/utils/getSpanKind.ts (80%) rename packages/{node-experimental => opentelemetry}/src/utils/groupSpansWithParents.ts (97%) create mode 100644 packages/opentelemetry/src/utils/isSentryRequest.ts create mode 100644 packages/opentelemetry/src/utils/mapStatus.ts create mode 100644 packages/opentelemetry/src/utils/parseSpanDescription.ts rename packages/{node-experimental/src/opentelemetry => opentelemetry/src/utils}/spanData.ts (100%) rename packages/{node-experimental => opentelemetry}/src/utils/spanTypes.ts (68%) rename packages/{node-experimental/test/sdk/otelAsyncContextStrategy.test.ts => opentelemetry/test/asyncContextStrategy.test.ts} (84%) create mode 100644 packages/opentelemetry/test/custom/client.test.ts rename packages/{node-experimental/test/sdk => opentelemetry/test/custom}/hub.test.ts (52%) rename packages/{node-experimental/test/sdk => opentelemetry/test/custom}/hubextensions.test.ts (52%) rename packages/{node-experimental/test/sdk => opentelemetry/test/custom}/scope.test.ts (81%) rename packages/{node-experimental/test/sdk => opentelemetry/test/custom}/transaction.test.ts (77%) create mode 100644 packages/opentelemetry/test/helpers/TestClient.ts create mode 100644 packages/opentelemetry/test/helpers/createSpan.ts create mode 100644 packages/opentelemetry/test/helpers/initOtel.ts create mode 100644 packages/opentelemetry/test/helpers/mockSdkInit.ts create mode 100644 packages/opentelemetry/test/integration/breadcrumbs.test.ts rename packages/{node-experimental => opentelemetry}/test/integration/otelTimedEvents.test.ts (76%) create mode 100644 packages/opentelemetry/test/integration/scope.test.ts create mode 100644 packages/opentelemetry/test/integration/transactions.test.ts rename packages/{node-experimental/test/opentelemetry => opentelemetry/test}/propagator.test.ts (90%) rename packages/{node-experimental/test/sdk => opentelemetry/test}/trace.test.ts (52%) create mode 100644 packages/opentelemetry/test/utils/captureExceptionForTimedEvent.test.ts rename packages/{node-experimental => opentelemetry}/test/utils/convertOtelTimeToSeconds.test.ts (100%) rename packages/{node-experimental => opentelemetry}/test/utils/getActiveSpan.test.ts (88%) rename packages/{node-experimental => opentelemetry}/test/utils/getRequestSpanData.test.ts (100%) create mode 100644 packages/opentelemetry/test/utils/getSpanKind.test.ts rename packages/{node-experimental => opentelemetry}/test/utils/groupSpansWithParents.test.ts (100%) create mode 100644 packages/opentelemetry/test/utils/mapStatus.test.ts create mode 100644 packages/opentelemetry/test/utils/parseSpanDescription.test.ts rename packages/{node-experimental => opentelemetry}/test/utils/setupEventContextTrace.test.ts (77%) rename packages/{node-experimental => opentelemetry}/test/utils/spanTypes.test.ts (72%) create mode 100644 packages/opentelemetry/tsconfig.json create mode 100644 packages/opentelemetry/tsconfig.test.json create mode 100644 packages/opentelemetry/tsconfig.types.json diff --git a/.craft.yml b/.craft.yml index 039528edc0a9..521e1e4f8ba3 100644 --- a/.craft.yml +++ b/.craft.yml @@ -24,6 +24,10 @@ targets: - name: npm id: '@sentry/replay' includeNames: /^sentry-replay-\d.*\.tgz$/ + ## 1.6. OpenTelemetry package + - name: npm + id: '@sentry/opentelemetry' + includeNames: /^sentry-opentelemetry-\d.*\.tgz$/ ## 2. Browser & Node SDKs - name: npm diff --git a/docs/new-sdk-release-checklist.md b/docs/new-sdk-release-checklist.md index 1b40763cdd6c..c02f0d01c1ef 100644 --- a/docs/new-sdk-release-checklist.md +++ b/docs/new-sdk-release-checklist.md @@ -47,6 +47,8 @@ This page serves as a checklist of what to do when releasing a new SDK for the f - [ ] Make sure it is added to `bundlePlugins.ts:makeTSPlugin` as `paths`, otherwise it will not be ES5 transpiled correctly for CDN builds. +- [ ] Make sure it is added to the [Verdaccio config](https://github.com/getsentry/sentry-javascript/blob/develop/packages/e2e-tests/verdaccio-config/config.yaml) for the E2E tests + ## Cutting the Release When you’re ready to make the first release, there are a couple of steps that need to be performed in the **correct order**. Note that you can prepare the PRs at any time but the **merging oder** is important: @@ -56,7 +58,7 @@ When you’re ready to make the first release, there are a couple of steps that ### Before the Release: - [ ] 1) If not yet done, be sure to remove the `private: true` property from your SDK’s `package.json`. Additionally, ensure that `"publishConfig": {"access": "public"}` is set. -- [ ] 2) Make sure that the new SDK is **not added** in`[craft.yml](https://github.com/getsentry/sentry-javascript/blob/master/.craft.yml)` as a target for the **Sentry release registry**\ +- [ ] 2) Make sure that the new SDK is **not added** in`[craft.yml](https://github.com/getsentry/sentry-javascript/blob/develop/.craft.yml)` as a target for the **Sentry release registry**\ *Once this is added, craft will try to publish an entry in the next release which does not work and caused failed release runs in the past* - [ ] 3) Add an `npm` target in `craft.yml` for the new package. Make sure to insert it in the right place, after all the Sentry dependencies of your package but before packages that depend on your new package (if applicable). ```yml @@ -74,7 +76,7 @@ When you’re ready to make the first release, there are a couple of steps that You have to fork this repo and PR the files from your fork to the main repo \ [Example PR](https://github.com/getsentry/sentry-release-registry/pull/80) from the Svelte SDK -- [ ] 2) Add an entry to `[craft.yml](https://github.com/getsentry/sentry-javascript/blob/master/.craft.yml)` to add releases of your SDK to the Sentry release registry \ +- [ ] 2) Add an entry to [craft.yml](https://github.com/getsentry/sentry-javascript/blob/develop/.craft.yml) to add releases of your SDK to the Sentry release registry \ [Example PR](https://github.com/getsentry/sentry-javascript/pull/5547) from the Svelte SDK \ *Subsequent releases will now be added automatically to the registry* diff --git a/package.json b/package.json index 2784d0c6be9d..4217fce71593 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "packages/node-integration-tests", "packages/node-experimental", "packages/opentelemetry-node", + "packages/opentelemetry", "packages/react", "packages/remix", "packages/replay", diff --git a/packages/e2e-tests/verdaccio-config/config.yaml b/packages/e2e-tests/verdaccio-config/config.yaml index 938b877a50e5..0f1fdee05669 100644 --- a/packages/e2e-tests/verdaccio-config/config.yaml +++ b/packages/e2e-tests/verdaccio-config/config.yaml @@ -122,6 +122,12 @@ packages: unpublish: $all # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/opentelemetry': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/react': access: $all publish: $all diff --git a/packages/node-experimental/README.md b/packages/node-experimental/README.md index 5bebaa336e17..e23ee4f1817c 100644 --- a/packages/node-experimental/README.md +++ b/packages/node-experimental/README.md @@ -82,7 +82,7 @@ const span = Sentry.startSpan({ description: 'non-active span' }); doSomethingSlow(); -span?.finish(); +span.finish(); ``` Finally you can also get the currently active span, if you need to do more with it: diff --git a/packages/node-experimental/package.json b/packages/node-experimental/package.json index a7f41fea10c8..dd3a5bd3afee 100644 --- a/packages/node-experimental/package.json +++ b/packages/node-experimental/package.json @@ -24,26 +24,26 @@ }, "dependencies": { "@opentelemetry/api": "~1.6.0", - "@opentelemetry/context-async-hooks": "~1.17.0", - "@opentelemetry/core": "~1.17.0", - "@opentelemetry/instrumentation": "~0.43.0", - "@opentelemetry/instrumentation-express": "~0.33.1", - "@opentelemetry/instrumentation-fastify": "~0.32.3", - "@opentelemetry/instrumentation-graphql": "~0.35.1", - "@opentelemetry/instrumentation-http": "~0.43.0", - "@opentelemetry/instrumentation-mongodb": "~0.37.0", - "@opentelemetry/instrumentation-mongoose": "~0.33.1", - "@opentelemetry/instrumentation-mysql": "~0.34.1", - "@opentelemetry/instrumentation-mysql2": "~0.34.1", - "@opentelemetry/instrumentation-nestjs-core": "~0.33.1", - "@opentelemetry/instrumentation-pg": "~0.36.1", - "@opentelemetry/resources": "~1.17.0", - "@opentelemetry/sdk-trace-base": "~1.17.0", - "@opentelemetry/semantic-conventions": "~1.17.0", - "@prisma/instrumentation": "~5.3.1", + "@opentelemetry/core": "~1.17.1", + "@opentelemetry/context-async-hooks": "~1.17.1", + "@opentelemetry/instrumentation": "0.44.0", + "@opentelemetry/instrumentation-express": "0.33.2", + "@opentelemetry/instrumentation-fastify": "0.32.3", + "@opentelemetry/instrumentation-graphql": "0.35.2", + "@opentelemetry/instrumentation-http": "0.44.0", + "@opentelemetry/instrumentation-mongodb": "0.37.1", + "@opentelemetry/instrumentation-mongoose": "0.33.2", + "@opentelemetry/instrumentation-mysql": "0.34.2", + "@opentelemetry/instrumentation-mysql2": "0.34.2", + "@opentelemetry/instrumentation-nestjs-core": "0.33.2", + "@opentelemetry/instrumentation-pg": "0.36.2", + "@opentelemetry/resources": "~1.17.1", + "@opentelemetry/sdk-trace-base": "~1.17.1", + "@opentelemetry/semantic-conventions": "~1.17.1", + "@prisma/instrumentation": "5.4.2", "@sentry/core": "7.74.1", "@sentry/node": "7.74.1", - "@sentry/opentelemetry-node": "7.74.1", + "@sentry/opentelemetry": "7.74.1", "@sentry/types": "7.74.1", "@sentry/utils": "7.74.1", "opentelemetry-instrumentation-fetch-node": "1.1.0" diff --git a/packages/node-experimental/src/constants.ts b/packages/node-experimental/src/constants.ts deleted file mode 100644 index 8d06aa411c1c..000000000000 --- a/packages/node-experimental/src/constants.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { createContextKey } from '@opentelemetry/api'; - -export const OTEL_CONTEXT_HUB_KEY = createContextKey('sentry_hub'); - -export const OTEL_ATTR_ORIGIN = 'sentry.origin'; -export const OTEL_ATTR_OP = 'sentry.op'; -export const OTEL_ATTR_SOURCE = 'sentry.source'; - -export const OTEL_ATTR_PARENT_SAMPLED = 'sentry.parentSampled'; - -export const OTEL_ATTR_BREADCRUMB_TYPE = 'sentry.breadcrumb.type'; -export const OTEL_ATTR_BREADCRUMB_LEVEL = 'sentry.breadcrumb.level'; -export const OTEL_ATTR_BREADCRUMB_EVENT_ID = 'sentry.breadcrumb.event_id'; -export const OTEL_ATTR_BREADCRUMB_CATEGORY = 'sentry.breadcrumb.category'; -export const OTEL_ATTR_BREADCRUMB_DATA = 'sentry.breadcrumb.data'; -export const OTEL_ATTR_SENTRY_SAMPLE_RATE = 'sentry.sample_rate'; - -export const SENTRY_TRACE_HEADER = 'sentry-trace'; -export const SENTRY_BAGGAGE_HEADER = 'baggage'; - -export const SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY = createContextKey('SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY'); diff --git a/packages/node-experimental/src/index.ts b/packages/node-experimental/src/index.ts index db1abe96495a..e19b1231712f 100644 --- a/packages/node-experimental/src/index.ts +++ b/packages/node-experimental/src/index.ts @@ -11,11 +11,10 @@ export { init } from './sdk/init'; export { INTEGRATIONS as Integrations }; export { getAutoPerformanceIntegrations } from './integrations/getAutoPerformanceIntegrations'; export * as Handlers from './sdk/handlers'; -export * from './sdk/trace'; -export { getActiveSpan } from './utils/getActiveSpan'; -export { getCurrentHub, getHubFromCarrier } from './sdk/hub'; export type { Span } from './types'; +export { startSpan, startInactiveSpan, getCurrentHub, getActiveSpan } from '@sentry/opentelemetry'; + export { makeNodeTransport, defaultStackParser, diff --git a/packages/node-experimental/src/integrations/http.ts b/packages/node-experimental/src/integrations/http.ts index 5b939e2ead20..69712e503761 100644 --- a/packages/node-experimental/src/integrations/http.ts +++ b/packages/node-experimental/src/integrations/http.ts @@ -3,17 +3,14 @@ import { SpanKind } from '@opentelemetry/api'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; import { hasTracingEnabled, isSentryRequestUrl } from '@sentry/core'; +import { _INTERNAL, getCurrentHub, getSpanKind, setSpanMetadata } from '@sentry/opentelemetry'; import type { EventProcessor, Hub, Integration } from '@sentry/types'; import { stringMatchesSomePattern } from '@sentry/utils'; import type { ClientRequest, IncomingMessage, ServerResponse } from 'http'; -import { OTEL_ATTR_ORIGIN } from '../constants'; -import { setSpanMetadata } from '../opentelemetry/spanData'; -import type { NodeExperimentalClient } from '../sdk/client'; -import { getCurrentHub } from '../sdk/hub'; -import { getRequestSpanData } from '../utils/getRequestSpanData'; +import type { NodeExperimentalClient } from '../types'; +import { addOriginToSpan } from '../utils/addOriginToSpan'; import { getRequestUrl } from '../utils/getRequestUrl'; -import { getSpanKind } from '../utils/getSpanKind'; interface HttpOptions { /** @@ -148,7 +145,7 @@ export class Http implements Integration { /** Update the span with data we need. */ private _updateSpan(span: Span, request: ClientRequest | IncomingMessage): void { - span.setAttribute(OTEL_ATTR_ORIGIN, 'auto.http.otel.http'); + addOriginToSpan(span, 'auto.http.otel.http'); if (getSpanKind(span) === SpanKind.SERVER) { setSpanMetadata(span, { request }); @@ -161,7 +158,7 @@ export class Http implements Integration { return; } - const data = getRequestSpanData(span); + const data = _INTERNAL.getRequestSpanData(span); getCurrentHub().addBreadcrumb( { category: 'http', diff --git a/packages/node-experimental/src/integrations/node-fetch.ts b/packages/node-experimental/src/integrations/node-fetch.ts index 9afd70be62e7..281c6f6d6784 100644 --- a/packages/node-experimental/src/integrations/node-fetch.ts +++ b/packages/node-experimental/src/integrations/node-fetch.ts @@ -2,14 +2,12 @@ import type { Span } from '@opentelemetry/api'; import { SpanKind } from '@opentelemetry/api'; import type { Instrumentation } from '@opentelemetry/instrumentation'; import { hasTracingEnabled } from '@sentry/core'; +import { _INTERNAL, getCurrentHub, getSpanKind } from '@sentry/opentelemetry'; import type { Integration } from '@sentry/types'; import { FetchInstrumentation } from 'opentelemetry-instrumentation-fetch-node'; -import { OTEL_ATTR_ORIGIN } from '../constants'; -import type { NodeExperimentalClient } from '../sdk/client'; -import { getCurrentHub } from '../sdk/hub'; -import { getRequestSpanData } from '../utils/getRequestSpanData'; -import { getSpanKind } from '../utils/getSpanKind'; +import type { NodeExperimentalClient } from '../types'; +import { addOriginToSpan } from '../utils/addOriginToSpan'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; interface NodeFetchOptions { @@ -101,7 +99,7 @@ export class NodeFetch extends NodePerformanceIntegration impl /** Update the span with data we need. */ private _updateSpan(span: Span): void { - span.setAttribute(OTEL_ATTR_ORIGIN, 'auto.http.otel.node_fetch'); + addOriginToSpan(span, 'auto.http.otel.node_fetch'); } /** Add a breadcrumb for outgoing requests. */ @@ -110,7 +108,7 @@ export class NodeFetch extends NodePerformanceIntegration impl return; } - const data = getRequestSpanData(span); + const data = _INTERNAL.getRequestSpanData(span); getCurrentHub().addBreadcrumb({ category: 'http', data: { diff --git a/packages/node-experimental/src/opentelemetry/contextManager.ts b/packages/node-experimental/src/opentelemetry/contextManager.ts deleted file mode 100644 index 438a2c49fac7..000000000000 --- a/packages/node-experimental/src/opentelemetry/contextManager.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { Context } from '@opentelemetry/api'; -import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; -import type { Carrier, Hub } from '@sentry/core'; - -import { OTEL_CONTEXT_HUB_KEY } from '../constants'; -import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from './../sdk/hub'; - -function createNewHub(parent: Hub | undefined): Hub { - const carrier: Carrier = {}; - ensureHubOnCarrier(carrier, parent); - return getHubFromCarrier(carrier); -} - -/** - * This is a custom ContextManager for OpenTelemetry, which extends the default AsyncLocalStorageContextManager. - * It ensures that we create a new hub per context, so that the OTEL Context & the Sentry Hub are always in sync. - * - * Note that we currently only support AsyncHooks with this, - * but since this should work for Node 14+ anyhow that should be good enough. - */ -export class SentryContextManager extends AsyncLocalStorageContextManager { - /** - * Overwrite with() of the original AsyncLocalStorageContextManager - * to ensure we also create a new hub per context. - */ - public with ReturnType>( - context: Context, - fn: F, - thisArg?: ThisParameterType, - ...args: A - ): ReturnType { - const existingHub = getCurrentHub(); - const newHub = createNewHub(existingHub); - - return super.with(context.setValue(OTEL_CONTEXT_HUB_KEY, newHub), fn, thisArg, ...args); - } -} diff --git a/packages/node-experimental/src/opentelemetry/spanProcessor.ts b/packages/node-experimental/src/opentelemetry/spanProcessor.ts deleted file mode 100644 index c7e07d11aa8e..000000000000 --- a/packages/node-experimental/src/opentelemetry/spanProcessor.ts +++ /dev/null @@ -1,99 +0,0 @@ -import type { Context } from '@opentelemetry/api'; -import { ROOT_CONTEXT, SpanKind, trace } from '@opentelemetry/api'; -import type { Span, SpanProcessor as SpanProcessorInterface } from '@opentelemetry/sdk-trace-base'; -import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'; -import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import { maybeCaptureExceptionForTimedEvent } from '@sentry/opentelemetry-node'; -import type { Hub } from '@sentry/types'; -import { logger } from '@sentry/utils'; - -import { OTEL_CONTEXT_HUB_KEY } from '../constants'; -import { Http } from '../integrations'; -import { NodeFetch } from '../integrations/node-fetch'; -import type { NodeExperimentalClient } from '../sdk/client'; -import { getCurrentHub } from '../sdk/hub'; -import { getSpanHub, setSpanHub, setSpanParent, setSpanScope } from './spanData'; -import { SentrySpanExporter } from './spanExporter'; - -/** - * Converts OpenTelemetry Spans to Sentry Spans and sends them to Sentry via - * the Sentry SDK. - */ -export class SentrySpanProcessor extends BatchSpanProcessor implements SpanProcessorInterface { - public constructor() { - super(new SentrySpanExporter()); - } - - /** - * @inheritDoc - */ - public onStart(span: Span, parentContext: Context): void { - // This is a reliable way to get the parent span - because this is exactly how the parent is identified in the OTEL SDK - const parentSpan = trace.getSpan(parentContext); - const hub = parentContext.getValue(OTEL_CONTEXT_HUB_KEY) as Hub | undefined; - - // We need access to the parent span in order to be able to move up the span tree for breadcrumbs - if (parentSpan) { - setSpanParent(span, parentSpan); - } - - // The root context does not have a hub stored, so we check for this specifically - // We do this instead of just falling back to `getCurrentHub` to avoid attaching the wrong hub - let actualHub = hub; - if (parentContext === ROOT_CONTEXT) { - actualHub = getCurrentHub(); - } - - // We need the scope at time of span creation in order to apply it to the event when the span is finished - if (actualHub) { - setSpanScope(span, actualHub.getScope()); - setSpanHub(span, actualHub); - } - - __DEBUG_BUILD__ && logger.log(`[Tracing] Starting span "${span.name}" (${span.spanContext().spanId})`); - - return super.onStart(span, parentContext); - } - - /** @inheritDoc */ - public onEnd(span: Span): void { - __DEBUG_BUILD__ && logger.log(`[Tracing] Finishing span "${span.name}" (${span.spanContext().spanId})`); - - if (!shouldCaptureSentrySpan(span)) { - // Prevent this being called to super.onEnd(), which would pass this to the span exporter - return; - } - - // Capture exceptions as events - const hub = getSpanHub(span) || getCurrentHub(); - span.events.forEach(event => { - maybeCaptureExceptionForTimedEvent(hub, event, span); - }); - - return super.onEnd(span); - } -} - -function shouldCaptureSentrySpan(span: Span): boolean { - const client = getCurrentHub().getClient(); - const httpIntegration = client ? client.getIntegration(Http) : undefined; - const fetchIntegration = client ? client.getIntegration(NodeFetch) : undefined; - - // If we encounter a client or server span with url & method, we assume this comes from the http instrumentation - // In this case, if `shouldCreateSpansForRequests` is false, we want to _record_ the span but not _sample_ it, - // So we can generate a breadcrumb for it but no span will be sent - if ( - (span.kind === SpanKind.CLIENT || span.kind === SpanKind.SERVER) && - span.attributes[SemanticAttributes.HTTP_URL] && - span.attributes[SemanticAttributes.HTTP_METHOD] - ) { - const shouldCreateSpansForRequests = - span.attributes['http.client'] === 'fetch' - ? fetchIntegration?.shouldCreateSpansForRequests - : httpIntegration?.shouldCreateSpansForRequests; - - return shouldCreateSpansForRequests !== false; - } - - return true; -} diff --git a/packages/node-experimental/src/sdk/client.ts b/packages/node-experimental/src/sdk/client.ts index a3145475e307..809d1fa49035 100644 --- a/packages/node-experimental/src/sdk/client.ts +++ b/packages/node-experimental/src/sdk/client.ts @@ -1,23 +1,7 @@ -import type { Tracer } from '@opentelemetry/api'; -import { trace } from '@opentelemetry/api'; -import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; -import type { EventHint, Scope } from '@sentry/node'; import { NodeClient, SDK_VERSION } from '@sentry/node'; -import type { Event } from '@sentry/types'; - -import type { - NodeExperimentalClient as NodeExperimentalClientInterface, - NodeExperimentalClientOptions, -} from '../types'; -import { NodeExperimentalScope } from './scope'; - -/** - * A client built on top of the NodeClient, which provides some otel-specific things on top. - */ -export class NodeExperimentalClient extends NodeClient implements NodeExperimentalClientInterface { - public traceProvider: BasicTracerProvider | undefined; - private _tracer: Tracer | undefined; +import { wrapClientClass } from '@sentry/opentelemetry'; +class NodeExperimentalBaseClient extends NodeClient { public constructor(options: ConstructorParameters[0]) { options._metadata = options._metadata || {}; options._metadata.sdk = options._metadata.sdk || { @@ -33,56 +17,6 @@ export class NodeExperimentalClient extends NodeClient implements NodeExperiment super(options); } - - /** Get the OTEL tracer. */ - public get tracer(): Tracer { - if (this._tracer) { - return this._tracer; - } - - const name = '@sentry/node-experimental'; - const version = SDK_VERSION; - const tracer = trace.getTracer(name, version); - this._tracer = tracer; - - return tracer; - } - - /** - * Get the options for the node preview client. - */ - public getOptions(): NodeExperimentalClientOptions { - // Just a type-cast, basically - return super.getOptions(); - } - - /** - * @inheritDoc - */ - public async flush(timeout?: number): Promise { - const provider = this.traceProvider; - const spanProcessor = provider?.activeSpanProcessor; - - if (spanProcessor) { - await spanProcessor.forceFlush(); - } - - return super.flush(timeout); - } - - /** - * Extends the base `_prepareEvent` so that we can properly handle `captureContext`. - * This uses `Scope.clone()`, which we need to replace with `NodeExperimentalScope.clone()` for this client. - */ - protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { - let actualScope = scope; - - // Remove `captureContext` hint and instead clone already here - if (hint && hint.captureContext) { - actualScope = NodeExperimentalScope.clone(scope); - delete hint.captureContext; - } - - return super._prepareEvent(event, hint, actualScope); - } } + +export const NodeExperimentalClient = wrapClientClass(NodeExperimentalBaseClient); diff --git a/packages/node-experimental/src/sdk/init.ts b/packages/node-experimental/src/sdk/init.ts index c33a90f037d7..be4843a5d2f7 100644 --- a/packages/node-experimental/src/sdk/init.ts +++ b/packages/node-experimental/src/sdk/init.ts @@ -1,5 +1,7 @@ import { hasTracingEnabled } from '@sentry/core'; +import type { NodeClient } from '@sentry/node'; import { defaultIntegrations as defaultNodeIntegrations, init as initNode } from '@sentry/node'; +import { setOpenTelemetryContextAsyncContextStrategy, setupGlobalHub } from '@sentry/opentelemetry'; import type { Integration } from '@sentry/types'; import { parseSemver } from '@sentry/utils'; @@ -8,9 +10,7 @@ import { Http } from '../integrations/http'; import { NodeFetch } from '../integrations/node-fetch'; import type { NodeExperimentalOptions } from '../types'; import { NodeExperimentalClient } from './client'; -import { getCurrentHub } from './hub'; import { initOtel } from './initOtel'; -import { setOtelContextAsyncContextStrategy } from './otelAsyncContextStrategy'; const NODE_VERSION: ReturnType = parseSemver(process.versions.node); const ignoredDefaultIntegrations = ['Http', 'Undici']; @@ -29,9 +29,7 @@ if (NODE_VERSION.major && NODE_VERSION.major >= 16) { * Initialize Sentry for Node. */ export function init(options: NodeExperimentalOptions | undefined = {}): void { - // Ensure we register our own global hub before something else does - // This will register the NodeExperimentalHub as the global hub - getCurrentHub(); + setupGlobalHub(); const isTracingEnabled = hasTracingEnabled(options); @@ -44,11 +42,11 @@ export function init(options: NodeExperimentalOptions | undefined = {}): void { ]; options.instrumenter = 'otel'; - options.clientClass = NodeExperimentalClient; + options.clientClass = NodeExperimentalClient as unknown as typeof NodeClient; initNode(options); // Always init Otel, even if tracing is disabled, because we need it for trace propagation & the HTTP integration initOtel(); - setOtelContextAsyncContextStrategy(); + setOpenTelemetryContextAsyncContextStrategy(); } diff --git a/packages/node-experimental/src/sdk/initOtel.ts b/packages/node-experimental/src/sdk/initOtel.ts index b60dac87aeda..271135728186 100644 --- a/packages/node-experimental/src/sdk/initOtel.ts +++ b/packages/node-experimental/src/sdk/initOtel.ts @@ -1,17 +1,20 @@ import { diag, DiagLogLevel } from '@opentelemetry/api'; +import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; import { Resource } from '@opentelemetry/resources'; import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import { SDK_VERSION } from '@sentry/core'; +import { + getCurrentHub, + SentryPropagator, + SentrySampler, + setupEventContextTrace, + wrapContextManagerClass, +} from '@sentry/opentelemetry'; import { logger } from '@sentry/utils'; -import { SentryPropagator } from '../opentelemetry/propagator'; -import { SentrySampler } from '../opentelemetry/sampler'; -import { SentrySpanProcessor } from '../opentelemetry/spanProcessor'; import type { NodeExperimentalClient } from '../types'; -import { setupEventContextTrace } from '../utils/setupEventContextTrace'; -import { SentryContextManager } from './../opentelemetry/contextManager'; -import { getCurrentHub } from './hub'; +import { NodeExperimentalSentrySpanProcessor } from './spanProcessor'; /** * Initialize OpenTelemetry for Node. @@ -56,15 +59,14 @@ export function setupOtel(client: NodeExperimentalClient): BasicTracerProvider { }), forceFlushTimeoutMillis: 500, }); - provider.addSpanProcessor(new SentrySpanProcessor()); + provider.addSpanProcessor(new NodeExperimentalSentrySpanProcessor()); - // We use a custom context manager to keep context in sync with sentry scope - const contextManager = new SentryContextManager(); + const SentryContextManager = wrapContextManagerClass(AsyncLocalStorageContextManager); // Initialize the provider provider.register({ propagator: new SentryPropagator(), - contextManager, + contextManager: new SentryContextManager(), }); return provider; diff --git a/packages/node-experimental/src/sdk/spanProcessor.ts b/packages/node-experimental/src/sdk/spanProcessor.ts new file mode 100644 index 000000000000..4d120b1e80e6 --- /dev/null +++ b/packages/node-experimental/src/sdk/spanProcessor.ts @@ -0,0 +1,39 @@ +import { SpanKind } from '@opentelemetry/api'; +import type { Span } from '@opentelemetry/sdk-trace-base'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { getCurrentHub, SentrySpanProcessor } from '@sentry/opentelemetry'; + +import { Http } from '../integrations/http'; +import { NodeFetch } from '../integrations/node-fetch'; +import type { NodeExperimentalClient } from '../types'; + +/** + * Implement custom code to avoid sending spans in certain cases. + */ +export class NodeExperimentalSentrySpanProcessor extends SentrySpanProcessor { + /** @inheritDoc */ + protected _shouldSendSpanToSentry(span: Span): boolean { + const client = getCurrentHub().getClient(); + const httpIntegration = client ? client.getIntegration(Http) : undefined; + const fetchIntegration = client ? client.getIntegration(NodeFetch) : undefined; + + // If we encounter a client or server span with url & method, we assume this comes from the http instrumentation + // In this case, if `shouldCreateSpansForRequests` is false, we want to _record_ the span but not _sample_ it, + // So we can generate a breadcrumb for it but no span will be sent + if ( + httpIntegration && + (span.kind === SpanKind.CLIENT || span.kind === SpanKind.SERVER) && + span.attributes[SemanticAttributes.HTTP_URL] && + span.attributes[SemanticAttributes.HTTP_METHOD] + ) { + const shouldCreateSpansForRequests = + span.attributes['http.client'] === 'fetch' + ? fetchIntegration?.shouldCreateSpansForRequests + : httpIntegration?.shouldCreateSpansForRequests; + + return shouldCreateSpansForRequests !== false; + } + + return true; + } +} diff --git a/packages/node-experimental/src/types.ts b/packages/node-experimental/src/types.ts index 8878a5fd2a8c..70384ddbd3f8 100644 --- a/packages/node-experimental/src/types.ts +++ b/packages/node-experimental/src/types.ts @@ -1,25 +1,15 @@ -import type { Span as WriteableSpan, Tracer } from '@opentelemetry/api'; -import type { BasicTracerProvider, ReadableSpan, Span } from '@opentelemetry/sdk-trace-base'; +import type { Span as WriteableSpan } from '@opentelemetry/api'; +import type { ReadableSpan, Span } from '@opentelemetry/sdk-trace-base'; import type { NodeClient, NodeOptions } from '@sentry/node'; -import type { SpanOrigin, TransactionMetadata, TransactionSource } from '@sentry/types'; +import type { OpenTelemetryClient } from '@sentry/opentelemetry'; export type NodeExperimentalOptions = NodeOptions; export type NodeExperimentalClientOptions = ConstructorParameters[0]; -export interface NodeExperimentalClient extends NodeClient { - tracer: Tracer; - traceProvider: BasicTracerProvider | undefined; +export interface NodeExperimentalClient extends NodeClient, OpenTelemetryClient { getOptions(): NodeExperimentalClientOptions; } -export interface NodeExperimentalSpanContext { - name: string; - op?: string; - metadata?: Partial; - origin?: SpanOrigin; - source?: TransactionSource; -} - /** * The base `Span` type is basically a `WriteableSpan`. * There are places where we basically want to allow passing _any_ span, diff --git a/packages/node-experimental/src/utils/addOriginToSpan.ts b/packages/node-experimental/src/utils/addOriginToSpan.ts index 007f55bb1e05..10fb7cf3402f 100644 --- a/packages/node-experimental/src/utils/addOriginToSpan.ts +++ b/packages/node-experimental/src/utils/addOriginToSpan.ts @@ -1,9 +1,8 @@ import type { Span } from '@opentelemetry/api'; +import { _INTERNAL } from '@sentry/opentelemetry'; import type { SpanOrigin } from '@sentry/types'; -import { OTEL_ATTR_ORIGIN } from '../constants'; - /** Adds an origin to an OTEL Span. */ export function addOriginToSpan(span: Span, origin: SpanOrigin): void { - span.setAttribute(OTEL_ATTR_ORIGIN, origin); + _INTERNAL.addOriginToSpan(span, origin); } diff --git a/packages/node-experimental/test/integration/breadcrumbs.test.ts b/packages/node-experimental/test/integration/breadcrumbs.test.ts index fbd46a6bd466..80842451c3bf 100644 --- a/packages/node-experimental/test/integration/breadcrumbs.test.ts +++ b/packages/node-experimental/test/integration/breadcrumbs.test.ts @@ -1,7 +1,7 @@ -import { withScope } from '../../src/'; -import { NodeExperimentalClient } from '../../src/sdk/client'; -import { getCurrentHub, NodeExperimentalHub } from '../../src/sdk/hub'; -import { startSpan } from '../../src/sdk/trace'; +import { withScope } from '@sentry/core'; +import { getCurrentHub, startSpan } from '@sentry/opentelemetry'; + +import type { NodeExperimentalClient } from '../../src/types'; import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; describe('Integration | breadcrumbs', () => { @@ -21,9 +21,6 @@ describe('Integration | breadcrumbs', () => { const hub = getCurrentHub(); const client = hub.getClient() as NodeExperimentalClient; - expect(hub).toBeInstanceOf(NodeExperimentalHub); - expect(client).toBeInstanceOf(NodeExperimentalClient); - hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); hub.addBreadcrumb({ timestamp: 123457, message: 'test2', data: { nested: 'yes' } }); hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); @@ -61,9 +58,6 @@ describe('Integration | breadcrumbs', () => { const hub = getCurrentHub(); const client = hub.getClient() as NodeExperimentalClient; - expect(hub).toBeInstanceOf(NodeExperimentalHub); - expect(client).toBeInstanceOf(NodeExperimentalClient); - const error = new Error('test'); hub.addBreadcrumb({ timestamp: 123456, message: 'test0' }); @@ -328,11 +322,11 @@ describe('Integration | breadcrumbs', () => { const promise2 = startSpan({ name: 'test-b' }, async () => { hub.addBreadcrumb({ timestamp: 123456, message: 'test1-b' }); - await startSpan({ name: 'inner1' }, async () => { + await startSpan({ name: 'inner1b' }, async () => { hub.addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); }); - await startSpan({ name: 'inner2' }, async () => { + await startSpan({ name: 'inner2b' }, async () => { hub.addBreadcrumb({ timestamp: 123455, message: 'test3-b' }); }); }); diff --git a/packages/node-experimental/test/integration/scope.test.ts b/packages/node-experimental/test/integration/scope.test.ts index 925047583f2e..5beb1b7ec06f 100644 --- a/packages/node-experimental/test/integration/scope.test.ts +++ b/packages/node-experimental/test/integration/scope.test.ts @@ -1,7 +1,7 @@ +import { getCurrentHub, getSpanScope } from '@sentry/opentelemetry'; + import * as Sentry from '../../src/'; -import { NodeExperimentalClient } from '../../src/sdk/client'; -import { getCurrentHub, NodeExperimentalHub } from '../../src/sdk/hub'; -import { NodeExperimentalScope } from '../../src/sdk/scope'; +import type { NodeExperimentalClient } from '../../src/types'; import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; describe('Integration | Scope', () => { @@ -24,10 +24,6 @@ describe('Integration | Scope', () => { const rootScope = hub.getScope(); - expect(hub).toBeInstanceOf(NodeExperimentalHub); - expect(rootScope).toBeInstanceOf(NodeExperimentalScope); - expect(client).toBeInstanceOf(NodeExperimentalClient); - const error = new Error('test error'); let spanId: string | undefined; let traceId: string | undefined; @@ -45,8 +41,10 @@ describe('Integration | Scope', () => { scope2.setTag('tag3', 'val3'); Sentry.startSpan({ name: 'outer' }, span => { - spanId = span?.spanContext().spanId; - traceId = span?.spanContext().traceId; + expect(getSpanScope(span)).toBe(enableTracing ? scope2 : undefined); + + spanId = span.spanContext().spanId; + traceId = span.spanContext().traceId; Sentry.setTag('tag4', 'val4'); @@ -96,7 +94,6 @@ describe('Integration | Scope', () => { trace_id: traceId, }, }), - spans: [], start_timestamp: expect.any(Number), tags: { @@ -127,10 +124,6 @@ describe('Integration | Scope', () => { const rootScope = hub.getScope(); - expect(hub).toBeInstanceOf(NodeExperimentalHub); - expect(rootScope).toBeInstanceOf(NodeExperimentalScope); - expect(client).toBeInstanceOf(NodeExperimentalClient); - const error1 = new Error('test error 1'); const error2 = new Error('test error 2'); let spanId1: string | undefined; @@ -147,8 +140,8 @@ describe('Integration | Scope', () => { scope2.setTag('tag3', 'val3a'); Sentry.startSpan({ name: 'outer' }, span => { - spanId1 = span?.spanContext().spanId; - traceId1 = span?.spanContext().traceId; + spanId1 = span.spanContext().spanId; + traceId1 = span.spanContext().traceId; Sentry.setTag('tag4', 'val4a'); @@ -164,8 +157,8 @@ describe('Integration | Scope', () => { scope2.setTag('tag3', 'val3b'); Sentry.startSpan({ name: 'outer' }, span => { - spanId2 = span?.spanContext().spanId; - traceId2 = span?.spanContext().traceId; + spanId2 = span.spanContext().spanId; + traceId2 = span.spanContext().traceId; Sentry.setTag('tag4', 'val4b'); diff --git a/packages/node-experimental/test/integration/transactions.test.ts b/packages/node-experimental/test/integration/transactions.test.ts index 4d657fc4cbf5..2f6cea23c0fd 100644 --- a/packages/node-experimental/test/integration/transactions.test.ts +++ b/packages/node-experimental/test/integration/transactions.test.ts @@ -1,16 +1,14 @@ import { context, SpanKind, trace, TraceFlags } from '@opentelemetry/api'; import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { getCurrentHub, SentrySpanProcessor, setPropagationContextOnContext } from '@sentry/opentelemetry'; import type { Integration, PropagationContext, TransactionEvent } from '@sentry/types'; import { logger } from '@sentry/utils'; import * as Sentry from '../../src'; import { startSpan } from '../../src'; -import { SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY } from '../../src/constants'; import type { Http, NodeFetch } from '../../src/integrations'; -import { SentrySpanProcessor } from '../../src/opentelemetry/spanProcessor'; -import type { NodeExperimentalClient } from '../../src/sdk/client'; -import { getCurrentHub } from '../../src/sdk/hub'; +import type { NodeExperimentalClient } from '../../src/types'; import { cleanupOtel, getProvider, mockSdkInit } from '../helpers/mockSdkInit'; describe('Integration | Transactions', () => { @@ -362,10 +360,7 @@ describe('Integration | Transactions', () => { // We simulate the correct context we'd normally get from the SentryPropagator context.with( - trace.setSpanContext( - context.active().setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext), - spanContext, - ), + trace.setSpanContext(setPropagationContextOnContext(context.active(), propagationContext), spanContext), () => { Sentry.startSpan({ op: 'test op', name: 'test name', source: 'task', origin: 'auto.test' }, span => { if (!span) { diff --git a/packages/opentelemetry-node/package.json b/packages/opentelemetry-node/package.json index 8087d8b7a29f..f49b31fdee7a 100644 --- a/packages/opentelemetry-node/package.json +++ b/packages/opentelemetry-node/package.json @@ -35,10 +35,10 @@ }, "devDependencies": { "@opentelemetry/api": "^1.6.0", - "@opentelemetry/core": "^1.7.0", - "@opentelemetry/sdk-trace-base": "^1.17.0", - "@opentelemetry/sdk-trace-node": "^1.17.0", - "@opentelemetry/semantic-conventions": "^1.17.0", + "@opentelemetry/core": "^1.17.1", + "@opentelemetry/sdk-trace-base": "^1.17.1", + "@opentelemetry/sdk-trace-node": "^1.17.1", + "@opentelemetry/semantic-conventions": "^1.17.1", "@sentry/node": "7.74.1" }, "scripts": { diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts index ea6e0833a395..fff3d0f4bc98 100644 --- a/packages/opentelemetry-node/test/spanprocessor.test.ts +++ b/packages/opentelemetry-node/test/spanprocessor.test.ts @@ -275,7 +275,7 @@ describe('SentrySpanProcessor', () => { 'service.name': 'test-service', 'telemetry.sdk.language': 'nodejs', 'telemetry.sdk.name': 'opentelemetry', - 'telemetry.sdk.version': '1.17.0', + 'telemetry.sdk.version': '1.17.1', }, }, }); @@ -300,7 +300,7 @@ describe('SentrySpanProcessor', () => { 'service.name': 'test-service', 'telemetry.sdk.language': 'nodejs', 'telemetry.sdk.name': 'opentelemetry', - 'telemetry.sdk.version': '1.17.0', + 'telemetry.sdk.version': '1.17.1', }, }, }); diff --git a/packages/opentelemetry/.eslintrc.js b/packages/opentelemetry/.eslintrc.js new file mode 100644 index 000000000000..9899ea1b73d8 --- /dev/null +++ b/packages/opentelemetry/.eslintrc.js @@ -0,0 +1,9 @@ +module.exports = { + env: { + node: true, + }, + extends: ['../../.eslintrc.js'], + rules: { + '@sentry-internal/sdk/no-optional-chaining': 'off', + }, +}; diff --git a/packages/opentelemetry/LICENSE b/packages/opentelemetry/LICENSE new file mode 100644 index 000000000000..d11896ba1181 --- /dev/null +++ b/packages/opentelemetry/LICENSE @@ -0,0 +1,14 @@ +Copyright (c) 2023 Sentry (https://sentry.io) and individual contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/opentelemetry/README.md b/packages/opentelemetry/README.md new file mode 100644 index 000000000000..2a5c88fa85cc --- /dev/null +++ b/packages/opentelemetry/README.md @@ -0,0 +1,98 @@ +

+ + Sentry + +

+ +# Official Sentry SDK for OpenTelemetry + +[![npm version](https://img.shields.io/npm/v/@sentry/opentelemetry.svg)](https://www.npmjs.com/package/@sentry/opentelemetry) +[![npm dm](https://img.shields.io/npm/dm/@sentry/opentelemetry.svg)](https://www.npmjs.com/package/@sentry/opentelemetry) +[![npm dt](https://img.shields.io/npm/dt/@sentry/opentelemetry.svg)](https://www.npmjs.com/package/@sentry/opentelemetry) + +This package allows you to send your OpenTelemetry trace data to Sentry via OpenTelemetry SpanProcessors. + +This SDK is **considered experimental and in an alpha state**. It may experience breaking changes. Please reach out on +[GitHub](https://github.com/getsentry/sentry-javascript/issues/new/choose) if you have any feedback/concerns. + +## Installation + +```bash +npm install @sentry/opentelemetry + +# Or yarn +yarn add @sentry/opentelemetry +``` + +Note that `@sentry/opentelemetry` depends on the following peer dependencies: + +- `@opentelemetry/api` version `1.0.0` or greater +- `@opentelemetry/core` version `1.0.0` or greater +- `@opentelemetry/semantic-conventions` version `1.0.0` or greater +- `@opentelemetry/sdk-trace-base` version `1.0.0` or greater, or a package that implements that, like + `@opentelemetry/sdk-node`. + +## Usage + +This package exposes a few building blocks you can add to your OpenTelemetry setup in order to capture OpenTelemetry traces to Sentry. + +This is how you can use this in your app: + +1. Setup the global hub for OpenTelemetry compatibility - ensure `setupGlobalHub()` is called before anything else! +1. Initialize Sentry, e.g. `@sentry/node` - make sure to set `instrumenter: 'otel'` in the SDK `init({})`! +1. Call `setupEventContextTrace(client)` +1. Add `SentrySampler` as sampler +1. Add `SentrySpanProcessor` as span processor +1. Add a context manager wrapped via `wrapContextManagerClass` +1. Add `SentryPropagator` as propagator +1. Setup OTEL-powered async context strategy for Sentry via `setOpenTelemetryContextAsyncContextStrategy()` + +For example, you could set this up as follows: + +```js +import * as Sentry from '@sentry/node'; +import { + getCurrentHub, + setupGlobalHub, + SentryPropagator, + SentrySampler, + SentrySpanProcessor, + setupEventContextTrace, + wrapContextManagerClass, + setOpenTelemetryContextAsyncContextStrategy, +} from '@sentry/opentelemetry'; +import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; + +function setupSentry() { + setupGlobalHub(); + + Sentry.init({ + dsn: 'xxx', + instrumenter: 'otel' + }); + + const client = getCurrentHub().getClient(); + setupEventContextTrace(client); + + const provider = new BasicTracerProvider({ + sampler: new SentrySampler(client), + }); + provider.addSpanProcessor(new SentrySpanProcessor()); + + const SentryContextManager = wrapContextManagerClass(AsyncLocalStorageContextManager); + + // Initialize the provider + provider.register({ + propagator: new SentryPropagator(), + contextManager: new SentryContextManager(), + }); + + setOpenTelemetryContextAsyncContextStrategy(); +} +``` + +A full setup example can be found in (node-experimental)[./../node-experimental]. + +## Links + +- [Official SDK Docs](https://docs.sentry.io/quickstart/) diff --git a/packages/opentelemetry/jest.config.js b/packages/opentelemetry/jest.config.js new file mode 100644 index 000000000000..24f49ab59a4c --- /dev/null +++ b/packages/opentelemetry/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../jest/jest.config.js'); diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json new file mode 100644 index 000000000000..a3064234dbac --- /dev/null +++ b/packages/opentelemetry/package.json @@ -0,0 +1,73 @@ +{ + "name": "@sentry/opentelemetry", + "version": "7.74.1", + "description": "Official Sentry utilities for OpenTelemetry", + "repository": "git://github.com/getsentry/sentry-javascript.git", + "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/opentelemetry", + "author": "Sentry", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "main": "build/cjs/index.js", + "module": "build/esm/index.js", + "types": "build/types/index.d.ts", + "typesVersions": { + "<4.9": { + "build/types/index.d.ts": [ + "build/types-ts3.8/index.d.ts" + ] + } + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@sentry/core": "7.74.1", + "@sentry/types": "7.74.1", + "@sentry/utils": "7.74.1" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@opentelemetry/core": "^1.0.0", + "@opentelemetry/sdk-trace-base": "^1.0.0", + "@opentelemetry/semantic-conventions": "^1.0.0" + }, + "devDependencies": { + "@opentelemetry/api": "^1.6.0", + "@opentelemetry/context-async-hooks": "^1.17.1", + "@opentelemetry/core": "^1.17.1", + "@opentelemetry/sdk-trace-base": "^1.17.1", + "@opentelemetry/sdk-trace-node": "^1.17.1", + "@opentelemetry/semantic-conventions": "^1.17.1" + }, + "scripts": { + "build": "run-p build:transpile build:types", + "build:dev": "yarn build", + "build:transpile": "rollup -c rollup.npm.config.js", + "build:types": "run-s build:types:core build:types:downlevel", + "build:types:core": "tsc -p tsconfig.types.json", + "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", + "build:watch": "run-p build:transpile:watch build:types:watch", + "build:dev:watch": "yarn build:watch", + "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", + "build:types:watch": "tsc -p tsconfig.types.json --watch", + "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", + "circularDepCheck": "madge --circular src/index.ts", + "clean": "rimraf build coverage sentry-opentelemetry-*.tgz", + "fix": "run-s fix:eslint fix:prettier", + "fix:eslint": "eslint . --format stylish --fix", + "fix:prettier": "prettier --write \"{src,test,scripts}/**/**.ts\"", + "lint": "run-s lint:prettier lint:eslint", + "lint:eslint": "eslint . --format stylish", + "lint:prettier": "prettier --check \"{src,test,scripts}/**/**.ts\"", + "test": "yarn test:jest", + "test:jest": "jest", + "test:watch": "jest --watch", + "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" + }, + "volta": { + "extends": "../../package.json" + }, + "sideEffects": false +} diff --git a/packages/opentelemetry/rollup.npm.config.js b/packages/opentelemetry/rollup.npm.config.js new file mode 100644 index 000000000000..5a62b528ef44 --- /dev/null +++ b/packages/opentelemetry/rollup.npm.config.js @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/node-experimental/src/sdk/otelAsyncContextStrategy.ts b/packages/opentelemetry/src/asyncContextStrategy.ts similarity index 82% rename from packages/node-experimental/src/sdk/otelAsyncContextStrategy.ts rename to packages/opentelemetry/src/asyncContextStrategy.ts index 7e4ca5cd4da0..c4cc48c1cfb5 100644 --- a/packages/node-experimental/src/sdk/otelAsyncContextStrategy.ts +++ b/packages/opentelemetry/src/asyncContextStrategy.ts @@ -2,18 +2,19 @@ import * as api from '@opentelemetry/api'; import type { Hub, RunWithAsyncContextOptions } from '@sentry/core'; import { setAsyncContextStrategy } from '@sentry/core'; -import { OTEL_CONTEXT_HUB_KEY } from '../constants'; +import { getHubFromContext } from './utils/contextData'; /** * Sets the async context strategy to use follow the OTEL context under the hood. * We handle forking a hub inside of our custom OTEL Context Manager (./otelContextManager.ts) */ -export function setOtelContextAsyncContextStrategy(): void { +export function setOpenTelemetryContextAsyncContextStrategy(): void { function getCurrentHub(): Hub | undefined { const ctx = api.context.active(); // Returning undefined means the global hub will be used - return ctx.getValue(OTEL_CONTEXT_HUB_KEY) as Hub | undefined; + // Need to cast from @sentry/type's `Hub` to @sentry/core's `Hub` + return getHubFromContext(ctx) as Hub | undefined; } /* This is more or less a NOOP - we rely on the OTEL context manager for this */ diff --git a/packages/opentelemetry/src/constants.ts b/packages/opentelemetry/src/constants.ts new file mode 100644 index 000000000000..36c8a36f886c --- /dev/null +++ b/packages/opentelemetry/src/constants.ts @@ -0,0 +1,10 @@ +import { createContextKey } from '@opentelemetry/api'; + +export const SENTRY_TRACE_HEADER = 'sentry-trace'; +export const SENTRY_BAGGAGE_HEADER = 'baggage'; + +/** Context Key to hold a PropagationContext. */ +export const SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY = createContextKey('SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY'); + +/** Context Key to hold a Hub. */ +export const SENTRY_HUB_CONTEXT_KEY = createContextKey('sentry_hub'); diff --git a/packages/opentelemetry/src/contextManager.ts b/packages/opentelemetry/src/contextManager.ts new file mode 100644 index 000000000000..ca9305dfea9b --- /dev/null +++ b/packages/opentelemetry/src/contextManager.ts @@ -0,0 +1,57 @@ +import type { Context, ContextManager } from '@opentelemetry/api'; +import type { Carrier, Hub } from '@sentry/core'; + +import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from './custom/hub'; +import { setHubOnContext } from './utils/contextData'; + +function createNewHub(parent: Hub | undefined): Hub { + const carrier: Carrier = {}; + ensureHubOnCarrier(carrier, parent); + return getHubFromCarrier(carrier); +} + +// Typescript complains if we do not use `...args: any[]` for the mixin, with: +// A mixin class must have a constructor with a single rest parameter of type 'any[]'.ts(2545) +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/** + * Wrap an OpenTelemetry ContextManager in a way that ensures the context is kept in sync with the Sentry Hub. + * + * Usage: + * import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; + * const SentryContextManager = wrapContextManagerClass(AsyncLocalStorageContextManager); + * const contextManager = new SentryContextManager(); + */ +export function wrapContextManagerClass( + ContextManagerClass: new (...args: any[]) => ContextManagerInstance, +): typeof ContextManagerClass { + /** + * This is a custom ContextManager for OpenTelemetry, which extends the default AsyncLocalStorageContextManager. + * It ensures that we create a new hub per context, so that the OTEL Context & the Sentry Hub are always in sync. + * + * Note that we currently only support AsyncHooks with this, + * but since this should work for Node 14+ anyhow that should be good enough. + */ + + // @ts-expect-error TS does not like this, but we know this is fine + class SentryContextManager extends ContextManagerClass { + /** + * Overwrite with() of the original AsyncLocalStorageContextManager + * to ensure we also create a new hub per context. + */ + public with ReturnType>( + context: Context, + fn: F, + thisArg?: ThisParameterType, + ...args: A + ): ReturnType { + const existingHub = getCurrentHub(); + const newHub = createNewHub(existingHub); + + return super.with(setHubOnContext(context, newHub), fn, thisArg, ...args); + } + } + + return SentryContextManager as unknown as typeof ContextManagerClass; +} +/* eslint-enable @typescript-eslint/no-explicit-any */ diff --git a/packages/opentelemetry/src/custom/client.ts b/packages/opentelemetry/src/custom/client.ts new file mode 100644 index 000000000000..adf348da60e1 --- /dev/null +++ b/packages/opentelemetry/src/custom/client.ts @@ -0,0 +1,81 @@ +import type { Tracer } from '@opentelemetry/api'; +import { trace } from '@opentelemetry/api'; +import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; +import type { BaseClient, Scope } from '@sentry/core'; +import { SDK_VERSION } from '@sentry/core'; +import type { Client, Event, EventHint } from '@sentry/types'; + +import type { OpenTelemetryClient as OpenTelemetryClientInterface } from '../types'; +import { OpenTelemetryScope } from './scope'; + +// Typescript complains if we do not use `...args: any[]` for the mixin, with: +// A mixin class must have a constructor with a single rest parameter of type 'any[]'.ts(2545) +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/** + * Wrap an Client with things we need for OpenTelemetry support. + * + * Usage: + * const OpenTelemetryClient = getWrappedClientClass(NodeClient); + * const client = new OpenTelemetryClient(options); + */ +export function wrapClientClass< + ClassConstructor extends new (...args: any[]) => Client & BaseClient, + WrappedClassConstructor extends new (...args: any[]) => Client & BaseClient & OpenTelemetryClientInterface, +>(ClientClass: ClassConstructor): WrappedClassConstructor { + class OpenTelemetryClient extends ClientClass implements OpenTelemetryClientInterface { + public traceProvider: BasicTracerProvider | undefined; + private _tracer: Tracer | undefined; + + public constructor(...args: any[]) { + super(...args); + } + + /** Get the OTEL tracer. */ + public get tracer(): Tracer { + if (this._tracer) { + return this._tracer; + } + + const name = '@sentry/opentelemetry'; + const version = SDK_VERSION; + const tracer = trace.getTracer(name, version); + this._tracer = tracer; + + return tracer; + } + + /** + * @inheritDoc + */ + public async flush(timeout?: number): Promise { + const provider = this.traceProvider; + const spanProcessor = provider?.activeSpanProcessor; + + if (spanProcessor) { + await spanProcessor.forceFlush(); + } + + return super.flush(timeout); + } + + /** + * Extends the base `_prepareEvent` so that we can properly handle `captureContext`. + * This uses `Scope.clone()`, which we need to replace with `NodeExperimentalScope.clone()` for this client. + */ + protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { + let actualScope = scope; + + // Remove `captureContext` hint and instead clone already here + if (hint && hint.captureContext) { + actualScope = OpenTelemetryScope.clone(scope); + delete hint.captureContext; + } + + return super._prepareEvent(event, hint, actualScope); + } + } + + return OpenTelemetryClient as unknown as WrappedClassConstructor; +} +/* eslint-enable @typescript-eslint/no-explicit-any */ diff --git a/packages/node-experimental/src/sdk/hub.ts b/packages/opentelemetry/src/custom/hub.ts similarity index 83% rename from packages/node-experimental/src/sdk/hub.ts rename to packages/opentelemetry/src/custom/hub.ts index 50958d13c84d..46dccd3c86e4 100644 --- a/packages/node-experimental/src/sdk/hub.ts +++ b/packages/opentelemetry/src/custom/hub.ts @@ -3,14 +3,14 @@ import { Hub } from '@sentry/core'; import type { Client } from '@sentry/types'; import { getGlobalSingleton, GLOBAL_OBJ } from '@sentry/utils'; -import { NodeExperimentalScope } from './scope'; +import { OpenTelemetryScope } from './scope'; /** * A custom hub that ensures we always creat an OTEL scope. * Exported only for testing */ -export class NodeExperimentalHub extends Hub { - public constructor(client?: Client, scope: Scope = new NodeExperimentalScope()) { +export class OpenTelemetryHub extends Hub { + public constructor(client?: Client, scope: Scope = new OpenTelemetryScope()) { super(client, scope); } @@ -19,7 +19,7 @@ export class NodeExperimentalHub extends Hub { */ public pushScope(): Scope { // We want to clone the content of prev scope - const scope = NodeExperimentalScope.clone(this.getScope()); + const scope = OpenTelemetryScope.clone(this.getScope()); this.getStack().push({ client: this.getClient(), scope, @@ -72,6 +72,20 @@ export function getCurrentHub(): Hub { return getGlobalHub(registry); } +/** + * Ensure the global hub is an OpenTelemetryHub. + */ +export function setupGlobalHub(): void { + const globalRegistry = getMainCarrier(); + + if (getGlobalHub(globalRegistry) instanceof OpenTelemetryHub) { + return; + } + + // If the current global hub is not correct, ensure we overwrite it + setHubOnCarrier(globalRegistry, new OpenTelemetryHub()); +} + /** * This will create a new {@link Hub} and add to the passed object on * __SENTRY__.hub. @@ -79,7 +93,7 @@ export function getCurrentHub(): Hub { * @hidden */ export function getHubFromCarrier(carrier: Carrier): Hub { - return getGlobalSingleton('hub', () => new NodeExperimentalHub(), carrier); + return getGlobalSingleton('hub', () => new OpenTelemetryHub(), carrier); } /** @@ -93,7 +107,7 @@ export function ensureHubOnCarrier(carrier: Carrier, parent: Hub = getGlobalHub( const globalHubTopStack = parent.getStackTop(); setHubOnCarrier( carrier, - new NodeExperimentalHub(globalHubTopStack.client, NodeExperimentalScope.clone(globalHubTopStack.scope)), + new OpenTelemetryHub(globalHubTopStack.client, OpenTelemetryScope.clone(globalHubTopStack.scope)), ); } } @@ -101,7 +115,7 @@ export function ensureHubOnCarrier(carrier: Carrier, parent: Hub = getGlobalHub( function getGlobalHub(registry: Carrier = getMainCarrier()): Hub { // If there's no hub, or its an old API, assign a new one if (!hasHubOnCarrier(registry) || getHubFromCarrier(registry).isOlderThan(API_VERSION)) { - setHubOnCarrier(registry, new NodeExperimentalHub()); + setHubOnCarrier(registry, new OpenTelemetryHub()); } // Return hub that lives on a global object diff --git a/packages/node-experimental/src/sdk/hubextensions.ts b/packages/opentelemetry/src/custom/hubextensions.ts similarity index 90% rename from packages/node-experimental/src/sdk/hubextensions.ts rename to packages/opentelemetry/src/custom/hubextensions.ts index 07ee08c1f7f9..4e839a9f3314 100644 --- a/packages/node-experimental/src/sdk/hubextensions.ts +++ b/packages/opentelemetry/src/custom/hubextensions.ts @@ -23,7 +23,7 @@ function startTransactionNoop( _customSamplingContext?: CustomSamplingContext, ): unknown { // eslint-disable-next-line no-console - console.warn('startTransaction is a noop in @sentry/node-experimental. Use `startSpan` instead.'); + console.warn('startTransaction is a noop in @sentry/opentelemetry. Use `startSpan` instead.'); // We return an object here as hub.ts checks for the result of this // and renders a different warning if this is empty return {}; diff --git a/packages/node-experimental/src/sdk/scope.ts b/packages/opentelemetry/src/custom/scope.ts similarity index 77% rename from packages/node-experimental/src/sdk/scope.ts rename to packages/opentelemetry/src/custom/scope.ts index 39f931936ccf..9c544b018134 100644 --- a/packages/node-experimental/src/sdk/scope.ts +++ b/packages/opentelemetry/src/custom/scope.ts @@ -4,20 +4,14 @@ import { Scope } from '@sentry/core'; import type { Breadcrumb, SeverityLevel, Span as SentrySpan } from '@sentry/types'; import { dateTimestampInSeconds, dropUndefinedKeys, logger, normalize } from '@sentry/utils'; -import { - OTEL_ATTR_BREADCRUMB_CATEGORY, - OTEL_ATTR_BREADCRUMB_DATA, - OTEL_ATTR_BREADCRUMB_EVENT_ID, - OTEL_ATTR_BREADCRUMB_LEVEL, - OTEL_ATTR_BREADCRUMB_TYPE, -} from '../constants'; -import { getSpanParent } from '../opentelemetry/spanData'; +import { InternalSentrySemanticAttributes } from '../semanticAttributes'; import { convertOtelTimeToSeconds } from '../utils/convertOtelTimeToSeconds'; import { getActiveSpan, getRootSpan } from '../utils/getActiveSpan'; +import { getSpanParent } from '../utils/spanData'; import { spanHasEvents } from '../utils/spanTypes'; /** A fork of the classic scope with some otel specific stuff. */ -export class NodeExperimentalScope extends Scope { +export class OpenTelemetryScope extends Scope { /** * This can be set to ensure the scope uses _this_ span as the active one, * instead of using getActiveSpan(). @@ -28,7 +22,7 @@ export class NodeExperimentalScope extends Scope { * @inheritDoc */ public static clone(scope?: Scope): Scope { - const newScope = new NodeExperimentalScope(); + const newScope = new OpenTelemetryScope(); if (scope) { newScope._breadcrumbs = [...scope['_breadcrumbs']]; newScope._tags = { ...scope['_tags'] }; @@ -55,7 +49,7 @@ export class NodeExperimentalScope extends Scope { */ public getSpan(): undefined { __DEBUG_BUILD__ && - logger.warn('Calling getSpan() is a noop in @sentry/node-experimental. Use `getActiveSpan()` instead.'); + logger.warn('Calling getSpan() is a noop in @sentry/opentelemetry. Use `getActiveSpan()` instead.'); return undefined; } @@ -65,8 +59,7 @@ export class NodeExperimentalScope extends Scope { * Instead, use the global `startSpan()` to define the active span. */ public setSpan(_span: SentrySpan): this { - __DEBUG_BUILD__ && - logger.warn('Calling setSpan() is a noop in @sentry/node-experimental. Use `startSpan()` instead.'); + __DEBUG_BUILD__ && logger.warn('Calling setSpan() is a noop in @sentry/opentelemetry. Use `startSpan()` instead.'); return this; } @@ -120,10 +113,10 @@ function breadcrumbToOtelEvent(breadcrumb: Breadcrumb): Parameters = (client && client.getOptions()) || {}; - const transaction = new NodeExperimentalTransaction(transactionContext, hub as Hub); + const transaction = new OpenTelemetryTransaction(transactionContext, hub as Hub); // Since we do not do sampling here, we assume that this is _always_ sampled // Any sampling decision happens in OpenTelemetry's sampler transaction.initSpanRecorder(options._experiments && (options._experiments.maxSpans as number)); @@ -25,7 +25,7 @@ export function startTransaction(hub: HubInterface, transactionContext: Transact /** * This is a fork of the base Transaction with OTEL specific stuff added. */ -export class NodeExperimentalTransaction extends Transaction { +export class OpenTelemetryTransaction extends Transaction { /** * Finish the transaction, but apply the given scope instead of the current one. */ diff --git a/packages/opentelemetry/src/index.ts b/packages/opentelemetry/src/index.ts new file mode 100644 index 000000000000..eb516e03dea1 --- /dev/null +++ b/packages/opentelemetry/src/index.ts @@ -0,0 +1,47 @@ +import { addOriginToSpan } from './utils/addOriginToSpan'; +import { maybeCaptureExceptionForTimedEvent } from './utils/captureExceptionForTimedEvent'; +import { getRequestSpanData } from './utils/getRequestSpanData'; + +export type { OpenTelemetryClient } from './types'; +export { wrapClientClass } from './custom/client'; + +export { getSpanKind } from './utils/getSpanKind'; +export { getSpanHub, getSpanMetadata, getSpanParent, getSpanScope, setSpanMetadata } from './utils/spanData'; + +export { getPropagationContextFromContext, setPropagationContextOnContext } from './utils/contextData'; + +export { + spanHasAttributes, + spanHasEvents, + spanHasKind, + spanHasName, + spanHasParentId, + spanHasStatus, +} from './utils/spanTypes'; + +export { isSentryRequestSpan } from './utils/isSentryRequest'; + +export { getActiveSpan, getRootSpan } from './utils/getActiveSpan'; +export { startSpan, startInactiveSpan } from './trace'; + +export { getCurrentHub, setupGlobalHub } from './custom/hub'; +export { addTracingExtensions } from './custom/hubextensions'; +export { setupEventContextTrace } from './setupEventContextTrace'; + +export { setOpenTelemetryContextAsyncContextStrategy } from './asyncContextStrategy'; +export { wrapContextManagerClass } from './contextManager'; +export { SentryPropagator } from './propagator'; +export { SentrySpanProcessor } from './spanProcessor'; +export { SentrySampler } from './sampler'; + +/** + * The following internal utils are not considered public API and are subject to change. + * @hidden + */ +const _INTERNAL = { + addOriginToSpan, + maybeCaptureExceptionForTimedEvent, + getRequestSpanData, +} as const; + +export { _INTERNAL }; diff --git a/packages/node-experimental/src/opentelemetry/propagator.ts b/packages/opentelemetry/src/propagator.ts similarity index 90% rename from packages/node-experimental/src/opentelemetry/propagator.ts rename to packages/opentelemetry/src/propagator.ts index 7aa43271b72c..db7a78d4f8f2 100644 --- a/packages/node-experimental/src/opentelemetry/propagator.ts +++ b/packages/opentelemetry/src/propagator.ts @@ -5,9 +5,10 @@ import { getDynamicSamplingContextFromClient } from '@sentry/core'; import type { DynamicSamplingContext, PropagationContext } from '@sentry/types'; import { generateSentryTraceHeader, SENTRY_BAGGAGE_KEY_PREFIX, tracingContextFromHeaders } from '@sentry/utils'; -import { getCurrentHub } from '../sdk/hub'; -import { SENTRY_BAGGAGE_HEADER, SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, SENTRY_TRACE_HEADER } from './../constants'; -import { getSpanScope } from './spanData'; +import { SENTRY_BAGGAGE_HEADER, SENTRY_TRACE_HEADER } from './constants'; +import { getCurrentHub } from './custom/hub'; +import { getPropagationContextFromContext, setPropagationContextOnContext } from './utils/contextData'; +import { getSpanScope } from './utils/spanData'; /** * Injects and extracts `sentry-trace` and `baggage` headers from carriers. @@ -23,12 +24,8 @@ export class SentryPropagator extends W3CBaggagePropagator { let baggage = propagation.getBaggage(context) || propagation.createBaggage({}); - const propagationContext = context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY) as - | PropagationContext - | undefined; - + const propagationContext = getPropagationContextFromContext(context); const { spanId, traceId, sampled } = getSentryTraceData(context, propagationContext); - const dynamicSamplingContext = propagationContext ? getDsc(context, propagationContext, traceId) : undefined; if (dynamicSamplingContext) { @@ -61,7 +58,7 @@ export class SentryPropagator extends W3CBaggagePropagator { const { propagationContext } = tracingContextFromHeaders(sentryTraceHeader, maybeBaggageHeader); // Add propagation context to context - const contextWithPropagationContext = context.setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext); + const contextWithPropagationContext = setPropagationContextOnContext(context, propagationContext); const spanContext: SpanContext = { traceId: propagationContext.traceId, diff --git a/packages/node-experimental/src/opentelemetry/sampler.ts b/packages/opentelemetry/src/sampler.ts similarity index 90% rename from packages/node-experimental/src/opentelemetry/sampler.ts rename to packages/opentelemetry/src/sampler.ts index 373c3b314b70..03f82f100a87 100644 --- a/packages/node-experimental/src/opentelemetry/sampler.ts +++ b/packages/opentelemetry/src/sampler.ts @@ -4,14 +4,11 @@ import { isSpanContextValid, trace, TraceFlags } from '@opentelemetry/api'; import type { Sampler, SamplingResult } from '@opentelemetry/sdk-trace-base'; import { SamplingDecision } from '@opentelemetry/sdk-trace-base'; import { hasTracingEnabled } from '@sentry/core'; -import type { Client, ClientOptions, PropagationContext, SamplingContext } from '@sentry/types'; +import type { Client, ClientOptions, SamplingContext } from '@sentry/types'; import { isNaN, logger } from '@sentry/utils'; -import { - OTEL_ATTR_PARENT_SAMPLED, - OTEL_ATTR_SENTRY_SAMPLE_RATE, - SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, -} from '../constants'; +import { InternalSentrySemanticAttributes } from './semanticAttributes'; +import { getPropagationContextFromContext } from './utils/contextData'; /** * A custom OTEL sampler that uses Sentry sampling rates to make it's decision @@ -65,11 +62,11 @@ export class SentrySampler implements Sampler { }); const attributes: Attributes = { - [OTEL_ATTR_SENTRY_SAMPLE_RATE]: Number(sampleRate), + [InternalSentrySemanticAttributes.SAMPLE_RATE]: Number(sampleRate), }; if (typeof parentSampled === 'boolean') { - attributes[OTEL_ATTR_PARENT_SAMPLED] = parentSampled; + attributes[InternalSentrySemanticAttributes.PARENT_SAMPLED] = parentSampled; } // Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The @@ -180,13 +177,9 @@ function isValidSampleRate(rate: unknown): boolean { return true; } -function getPropagationContext(parentContext: Context): PropagationContext | undefined { - return parentContext.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY) as PropagationContext | undefined; -} - function getParentRemoteSampled(spanContext: SpanContext, context: Context): boolean | undefined { const traceId = spanContext.traceId; - const traceparentData = getPropagationContext(context); + const traceparentData = getPropagationContextFromContext(context); // Only inherit sample rate if `traceId` is the same return traceparentData && traceId === traceparentData.traceId ? traceparentData.sampled : undefined; diff --git a/packages/opentelemetry/src/semanticAttributes.ts b/packages/opentelemetry/src/semanticAttributes.ts new file mode 100644 index 000000000000..00c37f061079 --- /dev/null +++ b/packages/opentelemetry/src/semanticAttributes.ts @@ -0,0 +1,16 @@ +/** + * These are internal and are not supposed to be used/depended on by external parties. + * No guarantees apply to these attributes, and the may change/disappear at any time. + */ +export const InternalSentrySemanticAttributes = { + ORIGIN: 'sentry.origin', + OP: 'sentry.op', + SOURCE: 'sentry.source', + SAMPLE_RATE: 'sentry.sample_rate', + PARENT_SAMPLED: 'sentry.parentSampled', + BREADCRUMB_TYPE: 'sentry.breadcrumb.type', + BREADCRUMB_LEVEL: 'sentry.breadcrumb.level', + BREADCRUMB_EVENT_ID: 'sentry.breadcrumb.event_id', + BREADCRUMB_CATEGORY: 'sentry.breadcrumb.category', + BREADCRUMB_DATA: 'sentry.breadcrumb.data', +} as const; diff --git a/packages/node-experimental/src/utils/setupEventContextTrace.ts b/packages/opentelemetry/src/setupEventContextTrace.ts similarity index 86% rename from packages/node-experimental/src/utils/setupEventContextTrace.ts rename to packages/opentelemetry/src/setupEventContextTrace.ts index 0e8dc7c23d7b..c55fc27ed52f 100644 --- a/packages/node-experimental/src/utils/setupEventContextTrace.ts +++ b/packages/opentelemetry/src/setupEventContextTrace.ts @@ -1,7 +1,7 @@ import type { Client } from '@sentry/types'; -import { getActiveSpan } from './getActiveSpan'; -import { spanHasParentId } from './spanTypes'; +import { getActiveSpan } from './utils/getActiveSpan'; +import { spanHasParentId } from './utils/spanTypes'; /** Ensure the `trace` context is set on all events. */ export function setupEventContextTrace(client: Client): void { diff --git a/packages/node-experimental/src/opentelemetry/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts similarity index 79% rename from packages/node-experimental/src/opentelemetry/spanExporter.ts rename to packages/opentelemetry/src/spanExporter.ts index e242f74d6104..f2094d132733 100644 --- a/packages/node-experimental/src/opentelemetry/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -2,29 +2,24 @@ import type { Span } from '@opentelemetry/api'; import { SpanKind } from '@opentelemetry/api'; import type { ExportResult } from '@opentelemetry/core'; import { ExportResultCode } from '@opentelemetry/core'; -import type { ReadableSpan, Span as SdkTraceBaseSpan, SpanExporter } from '@opentelemetry/sdk-trace-base'; +import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import { flush } from '@sentry/core'; -import { mapOtelStatus, parseOtelSpanDescription } from '@sentry/opentelemetry-node'; import type { DynamicSamplingContext, Span as SentrySpan, SpanOrigin, TransactionSource } from '@sentry/types'; import { logger } from '@sentry/utils'; -import { - OTEL_ATTR_OP, - OTEL_ATTR_ORIGIN, - OTEL_ATTR_PARENT_SAMPLED, - OTEL_ATTR_SENTRY_SAMPLE_RATE, - OTEL_ATTR_SOURCE, -} from '../constants'; -import { getCurrentHub } from '../sdk/hub'; -import { NodeExperimentalScope } from '../sdk/scope'; -import type { NodeExperimentalTransaction } from '../sdk/transaction'; -import { startTransaction } from '../sdk/transaction'; -import { convertOtelTimeToSeconds } from '../utils/convertOtelTimeToSeconds'; -import { getRequestSpanData } from '../utils/getRequestSpanData'; -import type { SpanNode } from '../utils/groupSpansWithParents'; -import { groupSpansWithParents } from '../utils/groupSpansWithParents'; -import { getSpanHub, getSpanMetadata, getSpanScope } from './spanData'; +import { getCurrentHub } from './custom/hub'; +import { OpenTelemetryScope } from './custom/scope'; +import type { OpenTelemetryTransaction } from './custom/transaction'; +import { startTransaction } from './custom/transaction'; +import { InternalSentrySemanticAttributes } from './semanticAttributes'; +import { convertOtelTimeToSeconds } from './utils/convertOtelTimeToSeconds'; +import { getRequestSpanData } from './utils/getRequestSpanData'; +import type { SpanNode } from './utils/groupSpansWithParents'; +import { groupSpansWithParents } from './utils/groupSpansWithParents'; +import { mapStatus } from './utils/mapStatus'; +import { parseSpanDescription } from './utils/parseSpanDescription'; +import { getSpanHub, getSpanMetadata, getSpanScope } from './utils/spanData'; type SpanNodeCompleted = SpanNode & { span: ReadableSpan }; @@ -117,9 +112,7 @@ function maybeSend(spans: ReadableSpan[]): ReadableSpan[] { // Now finish the transaction, which will send it together with all the spans // We make sure to use the current span as the activeSpan for this transaction const scope = getSpanScope(span); - const forkedScope = NodeExperimentalScope.clone( - scope as NodeExperimentalScope | undefined, - ) as NodeExperimentalScope; + const forkedScope = OpenTelemetryScope.clone(scope as OpenTelemetryScope | undefined) as OpenTelemetryScope; forkedScope.activeSpan = span as unknown as Span; transaction.finishWithScope(convertOtelTimeToSeconds(span.endTime), forkedScope); @@ -142,14 +135,14 @@ function shouldCleanupSpan(span: ReadableSpan, maxStartTimeOffsetSeconds: number function parseSpan(span: ReadableSpan): { op?: string; origin?: SpanOrigin; source?: TransactionSource } { const attributes = span.attributes; - const origin = attributes[OTEL_ATTR_ORIGIN] as SpanOrigin | undefined; - const op = attributes[OTEL_ATTR_OP] as string | undefined; - const source = attributes[OTEL_ATTR_SOURCE] as TransactionSource | undefined; + const origin = attributes[InternalSentrySemanticAttributes.ORIGIN] as SpanOrigin | undefined; + const op = attributes[InternalSentrySemanticAttributes.OP] as string | undefined; + const source = attributes[InternalSentrySemanticAttributes.SOURCE] as TransactionSource | undefined; return { origin, op, source }; } -function createTransactionForOtelSpan(span: ReadableSpan): NodeExperimentalTransaction { +function createTransactionForOtelSpan(span: ReadableSpan): OpenTelemetryTransaction { const scope = getSpanScope(span); const hub = getSpanHub(span) || getCurrentHub(); const spanContext = span.spanContext(); @@ -157,12 +150,12 @@ function createTransactionForOtelSpan(span: ReadableSpan): NodeExperimentalTrans const traceId = spanContext.traceId; const parentSpanId = span.parentSpanId; - const parentSampled = span.attributes[OTEL_ATTR_PARENT_SAMPLED] as boolean | undefined; + const parentSampled = span.attributes[InternalSentrySemanticAttributes.PARENT_SAMPLED] as boolean | undefined; const dynamicSamplingContext: DynamicSamplingContext | undefined = scope ? scope.getPropagationContext().dsc : undefined; - const { op, description, tags, data, origin, source } = getSpanData(span as SdkTraceBaseSpan); + const { op, description, tags, data, origin, source } = getSpanData(span); const metadata = getSpanMetadata(span); const transaction = startTransaction(hub, { @@ -173,19 +166,19 @@ function createTransactionForOtelSpan(span: ReadableSpan): NodeExperimentalTrans name: description, op, instrumenter: 'otel', - status: mapOtelStatus(span as SdkTraceBaseSpan), + status: mapStatus(span), startTimestamp: convertOtelTimeToSeconds(span.startTime), metadata: { dynamicSamplingContext, source, - sampleRate: span.attributes[OTEL_ATTR_SENTRY_SAMPLE_RATE] as number | undefined, + sampleRate: span.attributes[InternalSentrySemanticAttributes.SAMPLE_RATE] as number | undefined, ...metadata, }, data: removeSentryAttributes(data), origin, tags, sampled: true, - }) as NodeExperimentalTransaction; + }) as OpenTelemetryTransaction; transaction.setContext('otel', { attributes: removeSentryAttributes(span.attributes), @@ -212,14 +205,14 @@ function createAndFinishSpanForOtelSpan(node: SpanNode, sentryParentSpan: Sentry const spanId = span.spanContext().spanId; const { attributes } = span; - const { op, description, tags, data, origin } = getSpanData(span as SdkTraceBaseSpan); + const { op, description, tags, data, origin } = getSpanData(span); const allData = { ...removeSentryAttributes(attributes), ...data }; const sentrySpan = sentryParentSpan.startChild({ description, op, data: allData, - status: mapOtelStatus(span as SdkTraceBaseSpan), + status: mapStatus(span), instrumenter: 'otel', startTimestamp: convertOtelTimeToSeconds(span.startTime), spanId, @@ -243,12 +236,7 @@ function getSpanData(span: ReadableSpan): { origin?: SpanOrigin; } { const { op: definedOp, source: definedSource, origin } = parseSpan(span); - const { - op: inferredOp, - description, - source: inferredSource, - data: inferredData, - } = parseOtelSpanDescription(span as SdkTraceBaseSpan); + const { op: inferredOp, description, source: inferredSource, data: inferredData } = parseSpanDescription(span); const op = definedOp || inferredOp; const source = definedSource || inferredSource; @@ -274,11 +262,11 @@ function removeSentryAttributes(data: Record): Record { + maybeCaptureExceptionForTimedEvent(hub, event, span); + }); +} + +/** + * Converts OpenTelemetry Spans to Sentry Spans and sends them to Sentry via + * the Sentry SDK. + */ +export class SentrySpanProcessor extends BatchSpanProcessor implements SpanProcessorInterface { + public constructor() { + super(new SentrySpanExporter()); + } + + /** + * @inheritDoc + */ + public onStart(span: Span, parentContext: Context): void { + onSpanStart(span, parentContext); + + __DEBUG_BUILD__ && logger.log(`[Tracing] Starting span "${span.name}" (${span.spanContext().spanId})`); + + return super.onStart(span, parentContext); + } + + /** @inheritDoc */ + public onEnd(span: Span): void { + __DEBUG_BUILD__ && logger.log(`[Tracing] Finishing span "${span.name}" (${span.spanContext().spanId})`); + + if (!this._shouldSendSpanToSentry(span)) { + // Prevent this being called to super.onEnd(), which would pass this to the span exporter + return; + } + + onSpanEnd(span); + + return super.onEnd(span); + } + + /** + * You can overwrite this in a sub class to implement custom behavior for dropping spans. + * If you return `false` here, the span will not be passed to the exporter and thus not be sent. + */ + protected _shouldSendSpanToSentry(_span: Span): boolean { + return true; + } +} diff --git a/packages/node-experimental/src/sdk/trace.ts b/packages/opentelemetry/src/trace.ts similarity index 50% rename from packages/node-experimental/src/sdk/trace.ts rename to packages/opentelemetry/src/trace.ts index 72047f4478a3..e7a8fcaef8ea 100644 --- a/packages/node-experimental/src/sdk/trace.ts +++ b/packages/opentelemetry/src/trace.ts @@ -1,14 +1,13 @@ -import type { Tracer } from '@opentelemetry/api'; -import { SpanStatusCode } from '@opentelemetry/api'; -import type { Span } from '@opentelemetry/sdk-trace-base'; -import { hasTracingEnabled } from '@sentry/core'; +import type { Span, Tracer } from '@opentelemetry/api'; +import { SpanStatusCode, trace } from '@opentelemetry/api'; +import { SDK_VERSION } from '@sentry/core'; +import type { Client } from '@sentry/types'; import { isThenable } from '@sentry/utils'; -import { OTEL_ATTR_OP, OTEL_ATTR_ORIGIN, OTEL_ATTR_SOURCE } from '../constants'; -import { setSpanMetadata } from '../opentelemetry/spanData'; -import type { NodeExperimentalClient, NodeExperimentalSpanContext } from '../types'; -import { spanIsSdkTraceBaseSpan } from '../utils/spanTypes'; -import { getCurrentHub } from './hub'; +import { getCurrentHub } from './custom/hub'; +import { InternalSentrySemanticAttributes } from './semanticAttributes'; +import type { OpenTelemetryClient, OpenTelemetrySpanContext } from './types'; +import { setSpanMetadata } from './utils/spanData'; /** * Wraps a function with a transaction/span and finishes the span after the function is done. @@ -17,30 +16,18 @@ import { getCurrentHub } from './hub'; * * If you want to create a span that is not set as active, use {@link startInactiveSpan}. * - * Note that if you have not enabled tracing extensions via `addTracingExtensions` - * or you didn't set `tracesSampleRate`, this function will not generate spans - * and the `span` returned from the callback will be undefined. + * Note that you'll always get a span passed to the callback, it may just be a NonRecordingSpan if the span is not sampled. */ -export function startSpan(spanContext: NodeExperimentalSpanContext, callback: (span: Span | undefined) => T): T { +export function startSpan(spanContext: OpenTelemetrySpanContext, callback: (span: Span) => T): T { const tracer = getTracer(); - if (!tracer) { - return callback(undefined); - } const { name } = spanContext; - return tracer.startActiveSpan(name, (span): T => { + return tracer.startActiveSpan(name, span => { function finishSpan(): void { span.end(); } - // This is just a sanity check - in reality, this should not happen as we control the tracer, - // but to ensure type saftey we rather bail out here than to pass an invalid type out - if (!spanIsSdkTraceBaseSpan(span)) { - span.end(); - return callback(undefined); - } - _applySentryAttributesToSpan(span, spanContext); let maybePromiseResult: T; @@ -85,50 +72,36 @@ export const startActiveSpan = startSpan; * or you didn't set `tracesSampleRate` or `tracesSampler`, this function will not generate spans * and the `span` returned from the callback will be undefined. */ -export function startInactiveSpan(spanContext: NodeExperimentalSpanContext): Span | undefined { +export function startInactiveSpan(spanContext: OpenTelemetrySpanContext): Span { const tracer = getTracer(); - if (!tracer) { - return undefined; - } const { name } = spanContext; const span = tracer.startSpan(name); - // This is just a sanity check - in reality, this should not happen as we control the tracer, - // but to ensure type saftey we rather bail out here than to pass an invalid type out - if (!spanIsSdkTraceBaseSpan(span)) { - span.end(); - return undefined; - } - _applySentryAttributesToSpan(span, spanContext); return span; } -function getTracer(): Tracer | undefined { - if (!hasTracingEnabled()) { - return undefined; - } - - const client = getCurrentHub().getClient(); - return client && client.tracer; +function getTracer(): Tracer { + const client = getCurrentHub().getClient(); + return (client && client.tracer) || trace.getTracer('@sentry/opentelemetry', SDK_VERSION); } -function _applySentryAttributesToSpan(span: Span, spanContext: NodeExperimentalSpanContext): void { +function _applySentryAttributesToSpan(span: Span, spanContext: OpenTelemetrySpanContext): void { const { origin, op, source, metadata } = spanContext; if (origin) { - span.setAttribute(OTEL_ATTR_ORIGIN, origin); + span.setAttribute(InternalSentrySemanticAttributes.ORIGIN, origin); } if (op) { - span.setAttribute(OTEL_ATTR_OP, op); + span.setAttribute(InternalSentrySemanticAttributes.OP, op); } if (source) { - span.setAttribute(OTEL_ATTR_SOURCE, source); + span.setAttribute(InternalSentrySemanticAttributes.SOURCE, source); } if (metadata) { diff --git a/packages/opentelemetry/src/types.ts b/packages/opentelemetry/src/types.ts new file mode 100644 index 000000000000..0cb5342a3ac8 --- /dev/null +++ b/packages/opentelemetry/src/types.ts @@ -0,0 +1,29 @@ +import type { Span as WriteableSpan, Tracer } from '@opentelemetry/api'; +import type { BasicTracerProvider, ReadableSpan, Span } from '@opentelemetry/sdk-trace-base'; +import type { SpanOrigin, TransactionMetadata, TransactionSource } from '@sentry/types'; + +export interface OpenTelemetryClient { + tracer: Tracer; + traceProvider: BasicTracerProvider | undefined; +} + +export interface OpenTelemetrySpanContext { + name: string; + op?: string; + metadata?: Partial; + origin?: SpanOrigin; + source?: TransactionSource; +} + +/** + * The base `Span` type is basically a `WriteableSpan`. + * There are places where we basically want to allow passing _any_ span, + * so in these cases we type this as `AbstractSpan` which could be either a regular `Span` or a `ReadableSpan`. + * You'll have to make sure to check relevant fields before accessing them. + * + * Note that technically, the `Span` exported from `@opentelemwetry/sdk-trace-base` matches this, + * but we cannot be 100% sure that we are actually getting such a span, so this type is more defensive. + */ +export type AbstractSpan = WriteableSpan | ReadableSpan; + +export type { Span }; diff --git a/packages/opentelemetry/src/utils/addOriginToSpan.ts b/packages/opentelemetry/src/utils/addOriginToSpan.ts new file mode 100644 index 000000000000..d91d35085603 --- /dev/null +++ b/packages/opentelemetry/src/utils/addOriginToSpan.ts @@ -0,0 +1,9 @@ +import type { Span } from '@opentelemetry/api'; +import type { SpanOrigin } from '@sentry/types'; + +import { InternalSentrySemanticAttributes } from '../semanticAttributes'; + +/** Adds an origin to an OTEL Span. */ +export function addOriginToSpan(span: Span, origin: SpanOrigin): void { + span.setAttribute(InternalSentrySemanticAttributes.ORIGIN, origin); +} diff --git a/packages/opentelemetry/src/utils/captureExceptionForTimedEvent.ts b/packages/opentelemetry/src/utils/captureExceptionForTimedEvent.ts new file mode 100644 index 000000000000..3dde27e49e9f --- /dev/null +++ b/packages/opentelemetry/src/utils/captureExceptionForTimedEvent.ts @@ -0,0 +1,55 @@ +import type { Span as OtelSpan, TimedEvent } from '@opentelemetry/sdk-trace-base'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import type { Hub } from '@sentry/types'; +import { isString } from '@sentry/utils'; + +/** + * Maybe capture a Sentry exception for an OTEL timed event. + * This will check if the event is exception-like and in that case capture it as an exception. + */ +export function maybeCaptureExceptionForTimedEvent(hub: Hub, event: TimedEvent, otelSpan?: OtelSpan): void { + if (event.name !== 'exception') { + return; + } + + const attributes = event.attributes; + if (!attributes) { + return; + } + + const message = attributes[SemanticAttributes.EXCEPTION_MESSAGE]; + + if (typeof message !== 'string') { + return; + } + + const syntheticError = new Error(message); + + const stack = attributes[SemanticAttributes.EXCEPTION_STACKTRACE]; + if (isString(stack)) { + syntheticError.stack = stack; + } + + const type = attributes[SemanticAttributes.EXCEPTION_TYPE]; + if (isString(type)) { + syntheticError.name = type; + } + + hub.captureException(syntheticError, { + captureContext: otelSpan + ? { + contexts: { + otel: { + attributes: otelSpan.attributes, + resource: otelSpan.resource.attributes, + }, + trace: { + trace_id: otelSpan.spanContext().traceId, + span_id: otelSpan.spanContext().spanId, + parent_span_id: otelSpan.parentSpanId, + }, + }, + } + : undefined, + }); +} diff --git a/packages/opentelemetry/src/utils/contextData.ts b/packages/opentelemetry/src/utils/contextData.ts new file mode 100644 index 000000000000..899c55e3678d --- /dev/null +++ b/packages/opentelemetry/src/utils/contextData.ts @@ -0,0 +1,36 @@ +import type { Context } from '@opentelemetry/api'; +import type { Hub, PropagationContext } from '@sentry/types'; + +import { SENTRY_HUB_CONTEXT_KEY, SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY } from '../constants'; + +/** + * Try to get the Propagation Context from the given OTEL context. + * This requires the SentryPropagator to be registered. + */ +export function getPropagationContextFromContext(context: Context): PropagationContext | undefined { + return context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY) as PropagationContext | undefined; +} + +/** + * Set a Propagation Context on an OTEL context.. + * This will return a forked context with the Propagation Context set. + */ +export function setPropagationContextOnContext(context: Context, propagationContext: PropagationContext): Context { + return context.setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext); +} + +/** + * Try to get the Hub from the given OTEL context. + * This requires a Context Manager that was wrapped with getWrappedContextManager. + */ +export function getHubFromContext(context: Context): Hub | undefined { + return context.getValue(SENTRY_HUB_CONTEXT_KEY) as Hub | undefined; +} + +/** + * Set a Hub on an OTEL context.. + * This will return a forked context with the Propagation Context set. + */ +export function setHubOnContext(context: Context, hub: Hub): Context { + return context.setValue(SENTRY_HUB_CONTEXT_KEY, hub); +} diff --git a/packages/node-experimental/src/utils/convertOtelTimeToSeconds.ts b/packages/opentelemetry/src/utils/convertOtelTimeToSeconds.ts similarity index 100% rename from packages/node-experimental/src/utils/convertOtelTimeToSeconds.ts rename to packages/opentelemetry/src/utils/convertOtelTimeToSeconds.ts diff --git a/packages/node-experimental/src/utils/getActiveSpan.ts b/packages/opentelemetry/src/utils/getActiveSpan.ts similarity index 89% rename from packages/node-experimental/src/utils/getActiveSpan.ts rename to packages/opentelemetry/src/utils/getActiveSpan.ts index 240842770a68..1244a7cb4d62 100644 --- a/packages/node-experimental/src/utils/getActiveSpan.ts +++ b/packages/opentelemetry/src/utils/getActiveSpan.ts @@ -1,7 +1,7 @@ import type { Span } from '@opentelemetry/api'; import { trace } from '@opentelemetry/api'; -import { getSpanParent } from '../opentelemetry/spanData'; +import { getSpanParent } from './spanData'; /** * Returns the currently active span. diff --git a/packages/node-experimental/src/utils/getRequestSpanData.ts b/packages/opentelemetry/src/utils/getRequestSpanData.ts similarity index 100% rename from packages/node-experimental/src/utils/getRequestSpanData.ts rename to packages/opentelemetry/src/utils/getRequestSpanData.ts diff --git a/packages/node-experimental/src/utils/getSpanKind.ts b/packages/opentelemetry/src/utils/getSpanKind.ts similarity index 80% rename from packages/node-experimental/src/utils/getSpanKind.ts rename to packages/opentelemetry/src/utils/getSpanKind.ts index 7769a1cd3290..72a7407049b9 100644 --- a/packages/node-experimental/src/utils/getSpanKind.ts +++ b/packages/opentelemetry/src/utils/getSpanKind.ts @@ -1,6 +1,6 @@ -import type { Span } from '@opentelemetry/api'; import { SpanKind } from '@opentelemetry/api'; +import type { AbstractSpan } from '../types'; import { spanHasKind } from './spanTypes'; /** @@ -9,7 +9,7 @@ import { spanHasKind } from './spanTypes'; * so we need to check if we actually have a `SDKTraceBaseSpan` where we can fetch this from. * Otherwise, we fall back to `SpanKind.INTERNAL`. */ -export function getSpanKind(span: Span): SpanKind { +export function getSpanKind(span: AbstractSpan): SpanKind { if (spanHasKind(span)) { return span.kind; } diff --git a/packages/node-experimental/src/utils/groupSpansWithParents.ts b/packages/opentelemetry/src/utils/groupSpansWithParents.ts similarity index 97% rename from packages/node-experimental/src/utils/groupSpansWithParents.ts rename to packages/opentelemetry/src/utils/groupSpansWithParents.ts index 2af278d0bce2..6e5fb9e57c3a 100644 --- a/packages/node-experimental/src/utils/groupSpansWithParents.ts +++ b/packages/opentelemetry/src/utils/groupSpansWithParents.ts @@ -1,6 +1,6 @@ import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; -import { getSpanParent } from '../opentelemetry/spanData'; +import { getSpanParent } from './spanData'; export interface SpanNode { id: string; diff --git a/packages/opentelemetry/src/utils/isSentryRequest.ts b/packages/opentelemetry/src/utils/isSentryRequest.ts new file mode 100644 index 000000000000..361cc89d0ad7 --- /dev/null +++ b/packages/opentelemetry/src/utils/isSentryRequest.ts @@ -0,0 +1,26 @@ +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { getCurrentHub, isSentryRequestUrl } from '@sentry/core'; + +import type { AbstractSpan } from '../types'; +import { spanHasAttributes } from './spanTypes'; + +/** + * + * @param otelSpan Checks wheter a given OTEL Span is an http request to sentry. + * @returns boolean + */ +export function isSentryRequestSpan(span: AbstractSpan): boolean { + if (!spanHasAttributes(span)) { + return false; + } + + const { attributes } = span; + + const httpUrl = attributes[SemanticAttributes.HTTP_URL]; + + if (!httpUrl) { + return false; + } + + return isSentryRequestUrl(httpUrl.toString(), getCurrentHub()); +} diff --git a/packages/opentelemetry/src/utils/mapStatus.ts b/packages/opentelemetry/src/utils/mapStatus.ts new file mode 100644 index 000000000000..065a626d1c38 --- /dev/null +++ b/packages/opentelemetry/src/utils/mapStatus.ts @@ -0,0 +1,74 @@ +import { SpanStatusCode } from '@opentelemetry/api'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import type { SpanStatusType as SentryStatus } from '@sentry/core'; + +import type { AbstractSpan } from '../types'; +import { spanHasAttributes, spanHasStatus } from './spanTypes'; + +// canonicalCodesHTTPMap maps some HTTP codes to Sentry's span statuses. See possible mapping in https://develop.sentry.dev/sdk/event-payloads/span/ +const canonicalCodesHTTPMap: Record = { + '400': 'failed_precondition', + '401': 'unauthenticated', + '403': 'permission_denied', + '404': 'not_found', + '409': 'aborted', + '429': 'resource_exhausted', + '499': 'cancelled', + '500': 'internal_error', + '501': 'unimplemented', + '503': 'unavailable', + '504': 'deadline_exceeded', +} as const; + +// canonicalCodesGrpcMap maps some GRPC codes to Sentry's span statuses. See description in grpc documentation. +const canonicalCodesGrpcMap: Record = { + '1': 'cancelled', + '2': 'unknown_error', + '3': 'invalid_argument', + '4': 'deadline_exceeded', + '5': 'not_found', + '6': 'already_exists', + '7': 'permission_denied', + '8': 'resource_exhausted', + '9': 'failed_precondition', + '10': 'aborted', + '11': 'out_of_range', + '12': 'unimplemented', + '13': 'internal_error', + '14': 'unavailable', + '15': 'data_loss', + '16': 'unauthenticated', +} as const; + +/** + * Get a Sentry span status from an otel span. + */ +export function mapStatus(span: AbstractSpan): SentryStatus { + const attributes = spanHasAttributes(span) ? span.attributes : {}; + const status = spanHasStatus(span) ? span.status : undefined; + + const httpCode = attributes[SemanticAttributes.HTTP_STATUS_CODE]; + const grpcCode = attributes[SemanticAttributes.RPC_GRPC_STATUS_CODE]; + + const code = typeof httpCode === 'string' ? httpCode : typeof httpCode === 'number' ? httpCode.toString() : undefined; + if (code) { + const sentryStatus = canonicalCodesHTTPMap[code]; + if (sentryStatus) { + return sentryStatus; + } + } + + if (typeof grpcCode === 'string') { + const sentryStatus = canonicalCodesGrpcMap[grpcCode]; + if (sentryStatus) { + return sentryStatus; + } + } + + const statusCode = status && status.code; + if (statusCode === SpanStatusCode.OK || statusCode === SpanStatusCode.UNSET) { + return 'ok'; + } + + return 'unknown_error'; +} diff --git a/packages/opentelemetry/src/utils/parseSpanDescription.ts b/packages/opentelemetry/src/utils/parseSpanDescription.ts new file mode 100644 index 000000000000..784b268cc4f1 --- /dev/null +++ b/packages/opentelemetry/src/utils/parseSpanDescription.ts @@ -0,0 +1,166 @@ +import type { Attributes, AttributeValue } from '@opentelemetry/api'; +import { SpanKind } from '@opentelemetry/api'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import type { TransactionSource } from '@sentry/types'; +import { getSanitizedUrlString, parseUrl, stripUrlQueryAndFragment } from '@sentry/utils'; + +import type { AbstractSpan } from '../types'; +import { getSpanKind } from './getSpanKind'; +import { spanHasAttributes, spanHasName } from './spanTypes'; + +interface SpanDescription { + op: string | undefined; + description: string; + source: TransactionSource; + data?: Record; +} + +/** + * Extract better op/description from an otel span. + * + * Based on https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/7422ce2a06337f68a59b552b8c5a2ac125d6bae5/exporter/sentryexporter/sentry_exporter.go#L306 + */ +export function parseSpanDescription(span: AbstractSpan): SpanDescription { + const attributes = spanHasAttributes(span) ? span.attributes : {}; + const name = spanHasName(span) ? span.name : ''; + + // if http.method exists, this is an http request span + const httpMethod = attributes[SemanticAttributes.HTTP_METHOD]; + if (httpMethod) { + return descriptionForHttpMethod({ attributes, name, kind: getSpanKind(span) }, httpMethod); + } + + // If db.type exists then this is a database call span. + const dbSystem = attributes[SemanticAttributes.DB_SYSTEM]; + if (dbSystem) { + return descriptionForDbSystem({ attributes, name }); + } + + // If rpc.service exists then this is a rpc call span. + const rpcService = attributes[SemanticAttributes.RPC_SERVICE]; + if (rpcService) { + return { + op: 'rpc', + description: name, + source: 'route', + }; + } + + // If messaging.system exists then this is a messaging system span. + const messagingSystem = attributes[SemanticAttributes.MESSAGING_SYSTEM]; + if (messagingSystem) { + return { + op: 'message', + description: name, + source: 'route', + }; + } + + // If faas.trigger exists then this is a function as a service span. + const faasTrigger = attributes[SemanticAttributes.FAAS_TRIGGER]; + if (faasTrigger) { + return { op: faasTrigger.toString(), description: name, source: 'route' }; + } + + return { op: undefined, description: name, source: 'custom' }; +} + +function descriptionForDbSystem({ attributes, name }: { attributes: Attributes; name: string }): SpanDescription { + // Use DB statement (Ex "SELECT * FROM table") if possible as description. + const statement = attributes[SemanticAttributes.DB_STATEMENT]; + + const description = statement ? statement.toString() : name; + + return { op: 'db', description, source: 'task' }; +} + +/** Only exported for tests. */ +export function descriptionForHttpMethod( + { name, kind, attributes }: { name: string; attributes: Attributes; kind: SpanKind }, + httpMethod: AttributeValue, +): SpanDescription { + const opParts = ['http']; + + switch (kind) { + case SpanKind.CLIENT: + opParts.push('client'); + break; + case SpanKind.SERVER: + opParts.push('server'); + break; + } + + const { urlPath, url, query, fragment, hasRoute } = getSanitizedUrl(attributes, kind); + + if (!urlPath) { + return { op: opParts.join('.'), description: name, source: 'custom' }; + } + + // Ex. description="GET /api/users". + const description = `${httpMethod} ${urlPath}`; + + // If `httpPath` is a root path, then we can categorize the transaction source as route. + const source: TransactionSource = hasRoute || urlPath === '/' ? 'route' : 'url'; + + const data: Record = {}; + + if (url) { + data.url = url; + } + if (query) { + data['http.query'] = query; + } + if (fragment) { + data['http.fragment'] = fragment; + } + + return { + op: opParts.join('.'), + description, + source, + data, + }; +} + +/** Exported for tests only */ +export function getSanitizedUrl( + attributes: Attributes, + kind: SpanKind, +): { + url: string | undefined; + urlPath: string | undefined; + query: string | undefined; + fragment: string | undefined; + hasRoute: boolean; +} { + // This is the relative path of the URL, e.g. /sub + const httpTarget = attributes[SemanticAttributes.HTTP_TARGET]; + // This is the full URL, including host & query params etc., e.g. https://example.com/sub?foo=bar + const httpUrl = attributes[SemanticAttributes.HTTP_URL]; + // This is the normalized route name - may not always be available! + const httpRoute = attributes[SemanticAttributes.HTTP_ROUTE]; + + const parsedUrl = typeof httpUrl === 'string' ? parseUrl(httpUrl) : undefined; + const url = parsedUrl ? getSanitizedUrlString(parsedUrl) : undefined; + const query = parsedUrl && parsedUrl.search ? parsedUrl.search : undefined; + const fragment = parsedUrl && parsedUrl.hash ? parsedUrl.hash : undefined; + + if (typeof httpRoute === 'string') { + return { urlPath: httpRoute, url, query, fragment, hasRoute: true }; + } + + if (kind === SpanKind.SERVER && typeof httpTarget === 'string') { + return { urlPath: stripUrlQueryAndFragment(httpTarget), url, query, fragment, hasRoute: false }; + } + + if (parsedUrl) { + return { urlPath: url, url, query, fragment, hasRoute: false }; + } + + // fall back to target even for client spans, if no URL is present + if (typeof httpTarget === 'string') { + return { urlPath: stripUrlQueryAndFragment(httpTarget), url, query, fragment, hasRoute: false }; + } + + return { urlPath: undefined, url, query, fragment, hasRoute: false }; +} diff --git a/packages/node-experimental/src/opentelemetry/spanData.ts b/packages/opentelemetry/src/utils/spanData.ts similarity index 100% rename from packages/node-experimental/src/opentelemetry/spanData.ts rename to packages/opentelemetry/src/utils/spanData.ts diff --git a/packages/node-experimental/src/utils/spanTypes.ts b/packages/opentelemetry/src/utils/spanTypes.ts similarity index 68% rename from packages/node-experimental/src/utils/spanTypes.ts rename to packages/opentelemetry/src/utils/spanTypes.ts index 3883a97f8004..f92d411200a1 100644 --- a/packages/node-experimental/src/utils/spanTypes.ts +++ b/packages/opentelemetry/src/utils/spanTypes.ts @@ -1,6 +1,5 @@ -import type { SpanKind } from '@opentelemetry/api'; +import type { SpanKind, SpanStatus } from '@opentelemetry/api'; import type { ReadableSpan, TimedEvent } from '@opentelemetry/sdk-trace-base'; -import { Span as SdkTraceBaseSpan } from '@opentelemetry/sdk-trace-base'; import type { AbstractSpan } from '../types'; @@ -26,6 +25,28 @@ export function spanHasKind(span: SpanType): span return !!castSpan.kind; } +/** + * Check if a given span has a status. + * This is necessary because the base `Span` type does not have a status, + * so in places where we are passed a generic span, we need to check if we want to access it. + */ +export function spanHasStatus( + span: SpanType, +): span is SpanType & { status: SpanStatus } { + const castSpan = span as ReadableSpan; + return !!castSpan.status; +} + +/** + * Check if a given span has a name. + * This is necessary because the base `Span` type does not have a name, + * so in places where we are passed a generic span, we need to check if we want to access it. + */ +export function spanHasName(span: SpanType): span is SpanType & { name: string } { + const castSpan = span as ReadableSpan; + return !!castSpan.name; +} + /** * Check if a given span has a kind. * This is necessary because the base `Span` type does not have a kind, @@ -49,10 +70,3 @@ export function spanHasEvents( const castSpan = span as ReadableSpan; return Array.isArray(castSpan.events); } - -/** - * If the span is a SDK trace base span, which has some additional fields. - */ -export function spanIsSdkTraceBaseSpan(span: AbstractSpan): span is SdkTraceBaseSpan { - return span instanceof SdkTraceBaseSpan; -} diff --git a/packages/node-experimental/test/sdk/otelAsyncContextStrategy.test.ts b/packages/opentelemetry/test/asyncContextStrategy.test.ts similarity index 84% rename from packages/node-experimental/test/sdk/otelAsyncContextStrategy.test.ts rename to packages/opentelemetry/test/asyncContextStrategy.test.ts index 518d61000fee..7f6039a03c0e 100644 --- a/packages/node-experimental/test/sdk/otelAsyncContextStrategy.test.ts +++ b/packages/opentelemetry/test/asyncContextStrategy.test.ts @@ -2,21 +2,20 @@ import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import type { Hub } from '@sentry/core'; import { runWithAsyncContext, setAsyncContextStrategy } from '@sentry/core'; -import { NodeExperimentalClient } from '../../src/sdk/client'; -import { getCurrentHub } from '../../src/sdk/hub'; -import { setupOtel } from '../../src/sdk/initOtel'; -import { setOtelContextAsyncContextStrategy } from '../../src/sdk/otelAsyncContextStrategy'; -import { getDefaultNodeExperimentalClientOptions } from '../helpers/getDefaultNodePreviewClientOptions'; -import { cleanupOtel } from '../helpers/mockSdkInit'; - -describe('otelAsyncContextStrategy', () => { +import { setOpenTelemetryContextAsyncContextStrategy } from '../src/asyncContextStrategy'; +import { getCurrentHub } from '../src/custom/hub'; +import { setupOtel } from './helpers/initOtel'; +import { cleanupOtel } from './helpers/mockSdkInit'; +import { getDefaultTestClientOptions, TestClient } from './helpers/TestClient'; + +describe('asyncContextStrategy', () => { let provider: BasicTracerProvider | undefined; beforeEach(() => { - const options = getDefaultNodeExperimentalClientOptions(); - const client = new NodeExperimentalClient(options); + const options = getDefaultTestClientOptions(); + const client = new TestClient(options); provider = setupOtel(client); - setOtelContextAsyncContextStrategy(); + setOpenTelemetryContextAsyncContextStrategy(); }); afterEach(() => { diff --git a/packages/opentelemetry/test/custom/client.test.ts b/packages/opentelemetry/test/custom/client.test.ts new file mode 100644 index 000000000000..d377522c9c21 --- /dev/null +++ b/packages/opentelemetry/test/custom/client.test.ts @@ -0,0 +1,19 @@ +import { ProxyTracer } from '@opentelemetry/api'; + +import { getDefaultTestClientOptions, TestClient } from '../helpers/TestClient'; + +describe('OpenTelemetryClient', () => { + it('exposes a tracer', () => { + const options = getDefaultTestClientOptions(); + const client = new TestClient(options); + + const tracer = client.tracer; + expect(tracer).toBeDefined(); + expect(tracer).toBeInstanceOf(ProxyTracer); + + // Ensure we always get the same tracer instance + const tracer2 = client.tracer; + + expect(tracer2).toBe(tracer); + }); +}); diff --git a/packages/node-experimental/test/sdk/hub.test.ts b/packages/opentelemetry/test/custom/hub.test.ts similarity index 52% rename from packages/node-experimental/test/sdk/hub.test.ts rename to packages/opentelemetry/test/custom/hub.test.ts index a25de1565ad8..08e9b5e1bf90 100644 --- a/packages/node-experimental/test/sdk/hub.test.ts +++ b/packages/opentelemetry/test/custom/hub.test.ts @@ -1,43 +1,43 @@ -import { getCurrentHub, NodeExperimentalHub } from '../../src/sdk/hub'; -import { NodeExperimentalScope } from '../../src/sdk/scope'; +import { getCurrentHub, OpenTelemetryHub } from '../../src/custom/hub'; +import { OpenTelemetryScope } from '../../src/custom/scope'; -describe('NodeExperimentalHub', () => { +describe('OpenTelemetryHub', () => { it('getCurrentHub() returns the correct hub', () => { const hub = getCurrentHub(); expect(hub).toBeDefined(); - expect(hub).toBeInstanceOf(NodeExperimentalHub); + expect(hub).toBeInstanceOf(OpenTelemetryHub); const hub2 = getCurrentHub(); expect(hub2).toBe(hub); const scope = hub.getScope(); expect(scope).toBeDefined(); - expect(scope).toBeInstanceOf(NodeExperimentalScope); + expect(scope).toBeInstanceOf(OpenTelemetryScope); }); it('hub gets correct scope on initialization', () => { - const hub = new NodeExperimentalHub(); + const hub = new OpenTelemetryHub(); const scope = hub.getScope(); expect(scope).toBeDefined(); - expect(scope).toBeInstanceOf(NodeExperimentalScope); + expect(scope).toBeInstanceOf(OpenTelemetryScope); }); it('pushScope() creates correct scope', () => { - const hub = new NodeExperimentalHub(); + const hub = new OpenTelemetryHub(); const scope = hub.pushScope(); - expect(scope).toBeInstanceOf(NodeExperimentalScope); + expect(scope).toBeInstanceOf(OpenTelemetryScope); const scope2 = hub.getScope(); expect(scope2).toBe(scope); }); it('withScope() creates correct scope', () => { - const hub = new NodeExperimentalHub(); + const hub = new OpenTelemetryHub(); hub.withScope(scope => { - expect(scope).toBeInstanceOf(NodeExperimentalScope); + expect(scope).toBeInstanceOf(OpenTelemetryScope); }); }); }); diff --git a/packages/node-experimental/test/sdk/hubextensions.test.ts b/packages/opentelemetry/test/custom/hubextensions.test.ts similarity index 52% rename from packages/node-experimental/test/sdk/hubextensions.test.ts rename to packages/opentelemetry/test/custom/hubextensions.test.ts index c2fee6baabde..47f6452062cc 100644 --- a/packages/node-experimental/test/sdk/hubextensions.test.ts +++ b/packages/opentelemetry/test/custom/hubextensions.test.ts @@ -1,7 +1,6 @@ -import { NodeExperimentalClient } from '../../src/sdk/client'; -import { getCurrentHub } from '../../src/sdk/hub'; -import { addTracingExtensions } from '../../src/sdk/hubextensions'; -import { getDefaultNodeExperimentalClientOptions } from '../helpers/getDefaultNodePreviewClientOptions'; +import { getCurrentHub } from '../../src/custom/hub'; +import { addTracingExtensions } from '../../src/custom/hubextensions'; +import { getDefaultTestClientOptions, TestClient } from '../helpers/TestClient'; describe('hubextensions', () => { afterEach(() => { @@ -9,7 +8,7 @@ describe('hubextensions', () => { }); it('startTransaction is noop', () => { - const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions()); + const client = new TestClient(getDefaultTestClientOptions()); getCurrentHub().bindClient(client); addTracingExtensions(); @@ -20,7 +19,7 @@ describe('hubextensions', () => { expect(mockConsole).toHaveBeenCalledTimes(1); expect(mockConsole).toHaveBeenCalledWith( - 'startTransaction is a noop in @sentry/node-experimental. Use `startSpan` instead.', + 'startTransaction is a noop in @sentry/opentelemetry. Use `startSpan` instead.', ); }); }); diff --git a/packages/node-experimental/test/sdk/scope.test.ts b/packages/opentelemetry/test/custom/scope.test.ts similarity index 81% rename from packages/node-experimental/test/sdk/scope.test.ts rename to packages/opentelemetry/test/custom/scope.test.ts index 7d8d772abd8c..6c96fab2f3c5 100644 --- a/packages/node-experimental/test/sdk/scope.test.ts +++ b/packages/opentelemetry/test/custom/scope.test.ts @@ -1,15 +1,9 @@ import { makeSession } from '@sentry/core'; import type { Breadcrumb } from '@sentry/types'; -import { - OTEL_ATTR_BREADCRUMB_CATEGORY, - OTEL_ATTR_BREADCRUMB_DATA, - OTEL_ATTR_BREADCRUMB_EVENT_ID, - OTEL_ATTR_BREADCRUMB_LEVEL, - OTEL_ATTR_BREADCRUMB_TYPE, -} from '../../src/constants'; -import { setSpanParent } from '../../src/opentelemetry/spanData'; -import { NodeExperimentalScope } from '../../src/sdk/scope'; +import { OpenTelemetryScope } from '../../src/custom/scope'; +import { InternalSentrySemanticAttributes } from '../../src/semanticAttributes'; +import { setSpanParent } from '../../src/utils/spanData'; import { createSpan } from '../helpers/createSpan'; import * as GetActiveSpan from './../../src/utils/getActiveSpan'; @@ -19,7 +13,7 @@ describe('NodeExperimentalScope', () => { }); it('clone() correctly clones the scope', () => { - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); scope['_breadcrumbs'] = [{ message: 'test' }]; scope['_tags'] = { tag: 'bar' }; @@ -36,9 +30,9 @@ describe('NodeExperimentalScope', () => { scope['_attachments'] = [{ data: '123', filename: 'test.txt' }]; scope['_sdkProcessingMetadata'] = { sdk: 'bar' }; - const scope2 = NodeExperimentalScope.clone(scope); + const scope2 = OpenTelemetryScope.clone(scope); - expect(scope2).toBeInstanceOf(NodeExperimentalScope); + expect(scope2).toBeInstanceOf(OpenTelemetryScope); expect(scope2).not.toBe(scope); // Ensure everything is correctly cloned @@ -74,13 +68,13 @@ describe('NodeExperimentalScope', () => { }); it('clone() works without existing scope', () => { - const scope = NodeExperimentalScope.clone(undefined); + const scope = OpenTelemetryScope.clone(undefined); - expect(scope).toBeInstanceOf(NodeExperimentalScope); + expect(scope).toBeInstanceOf(OpenTelemetryScope); }); it('getSpan returns undefined', () => { - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); // Pretend we have a _span set scope['_span'] = {} as any; @@ -89,7 +83,7 @@ describe('NodeExperimentalScope', () => { }); it('setSpan is a noop', () => { - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); scope.setSpan({} as any); @@ -100,7 +94,7 @@ describe('NodeExperimentalScope', () => { it('adds to scope if no root span is found', () => { jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(undefined); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const breadcrumb: Breadcrumb = { message: 'test' }; const now = Date.now(); @@ -115,7 +109,7 @@ describe('NodeExperimentalScope', () => { it('adds to scope if no root span is found & uses given timestamp', () => { jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(undefined); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const breadcrumb: Breadcrumb = { message: 'test', timestamp: 1234 }; scope.addBreadcrumb(breadcrumb); @@ -127,7 +121,7 @@ describe('NodeExperimentalScope', () => { const span = createSpan(); jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const breadcrumb: Breadcrumb = { message: 'test' }; const now = Date.now(); @@ -150,7 +144,7 @@ describe('NodeExperimentalScope', () => { const span = createSpan(); jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const breadcrumb: Breadcrumb = { timestamp: 12345, message: 'test' }; scope.addBreadcrumb(breadcrumb); @@ -169,7 +163,7 @@ describe('NodeExperimentalScope', () => { const span = createSpan(); jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const breadcrumb1: Breadcrumb = { timestamp: 12345, message: 'test1' }; const breadcrumb2: Breadcrumb = { timestamp: 5678, message: 'test2' }; const breadcrumb3: Breadcrumb = { timestamp: 9101112, message: 'test3' }; @@ -202,7 +196,7 @@ describe('NodeExperimentalScope', () => { const span = createSpan(); jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const breadcrumb: Breadcrumb = { timestamp: 12345 }; scope.addBreadcrumb(breadcrumb); @@ -221,7 +215,7 @@ describe('NodeExperimentalScope', () => { const span = createSpan(); jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const breadcrumb: Breadcrumb = { timestamp: 12345, message: 'test', @@ -240,11 +234,11 @@ describe('NodeExperimentalScope', () => { name: 'test', time: [12345, 0], attributes: { - [OTEL_ATTR_BREADCRUMB_DATA]: JSON.stringify({ nested: { indeed: true } }), - [OTEL_ATTR_BREADCRUMB_TYPE]: 'test-type', - [OTEL_ATTR_BREADCRUMB_LEVEL]: 'info', - [OTEL_ATTR_BREADCRUMB_EVENT_ID]: 'test-event-id', - [OTEL_ATTR_BREADCRUMB_CATEGORY]: 'test-category', + [InternalSentrySemanticAttributes.BREADCRUMB_DATA]: JSON.stringify({ nested: { indeed: true } }), + [InternalSentrySemanticAttributes.BREADCRUMB_TYPE]: 'test-type', + [InternalSentrySemanticAttributes.BREADCRUMB_LEVEL]: 'info', + [InternalSentrySemanticAttributes.BREADCRUMB_EVENT_ID]: 'test-event-id', + [InternalSentrySemanticAttributes.BREADCRUMB_CATEGORY]: 'test-category', }, }), ]); @@ -254,7 +248,7 @@ describe('NodeExperimentalScope', () => { const span = createSpan(); jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const breadcrumb: Breadcrumb = { timestamp: 12345, message: 'test', data: {} }; scope.addBreadcrumb(breadcrumb); @@ -274,7 +268,7 @@ describe('NodeExperimentalScope', () => { it('gets from scope if no root span is found', () => { jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(undefined); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const breadcrumbs: Breadcrumb[] = [ { message: 'test1', timestamp: 1234 }, { message: 'test2', timestamp: 12345 }, @@ -289,7 +283,7 @@ describe('NodeExperimentalScope', () => { const span = createSpan(); jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const now = Date.now(); @@ -298,18 +292,18 @@ describe('NodeExperimentalScope', () => { span.addEvent( 'breadcrumb event 2', { - [OTEL_ATTR_BREADCRUMB_DATA]: JSON.stringify({ nested: { indeed: true } }), - [OTEL_ATTR_BREADCRUMB_TYPE]: 'test-type', - [OTEL_ATTR_BREADCRUMB_LEVEL]: 'info', - [OTEL_ATTR_BREADCRUMB_EVENT_ID]: 'test-event-id', - [OTEL_ATTR_BREADCRUMB_CATEGORY]: 'test-category', + [InternalSentrySemanticAttributes.BREADCRUMB_DATA]: JSON.stringify({ nested: { indeed: true } }), + [InternalSentrySemanticAttributes.BREADCRUMB_TYPE]: 'test-type', + [InternalSentrySemanticAttributes.BREADCRUMB_LEVEL]: 'info', + [InternalSentrySemanticAttributes.BREADCRUMB_EVENT_ID]: 'test-event-id', + [InternalSentrySemanticAttributes.BREADCRUMB_CATEGORY]: 'test-category', }, now + 3000, ); span.addEvent( 'breadcrumb event invalid JSON data', { - [OTEL_ATTR_BREADCRUMB_DATA]: 'this is not JSON...', + [InternalSentrySemanticAttributes.BREADCRUMB_DATA]: 'this is not JSON...', }, now + 2000, ); @@ -339,7 +333,7 @@ describe('NodeExperimentalScope', () => { setSpanParent(span, parentSpan); setSpanParent(parentSpan, rootSpan); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const now = Date.now(); @@ -348,14 +342,14 @@ describe('NodeExperimentalScope', () => { span.addEvent( 'breadcrumb event 2', { - [OTEL_ATTR_BREADCRUMB_DATA]: JSON.stringify({ nested: true }), + [InternalSentrySemanticAttributes.BREADCRUMB_DATA]: JSON.stringify({ nested: true }), }, now + 3000, ); rootSpan.addEvent( 'breadcrumb event invalid JSON data', { - [OTEL_ATTR_BREADCRUMB_DATA]: 'this is not JSON...', + [InternalSentrySemanticAttributes.BREADCRUMB_DATA]: 'this is not JSON...', }, now + 2000, ); @@ -372,7 +366,7 @@ describe('NodeExperimentalScope', () => { const span = createSpan(); jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const breadcrumbs: Breadcrumb[] = [ { message: 'test1', timestamp: 1234 }, @@ -399,7 +393,7 @@ describe('NodeExperimentalScope', () => { const span = createSpan(); jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const now = Date.now(); @@ -408,18 +402,18 @@ describe('NodeExperimentalScope', () => { span.addEvent( 'breadcrumb event 2', { - [OTEL_ATTR_BREADCRUMB_DATA]: JSON.stringify({ nested: { indeed: true } }), - [OTEL_ATTR_BREADCRUMB_TYPE]: 'test-type', - [OTEL_ATTR_BREADCRUMB_LEVEL]: 'info', - [OTEL_ATTR_BREADCRUMB_EVENT_ID]: 'test-event-id', - [OTEL_ATTR_BREADCRUMB_CATEGORY]: 'test-category', + [InternalSentrySemanticAttributes.BREADCRUMB_DATA]: JSON.stringify({ nested: { indeed: true } }), + [InternalSentrySemanticAttributes.BREADCRUMB_TYPE]: 'test-type', + [InternalSentrySemanticAttributes.BREADCRUMB_LEVEL]: 'info', + [InternalSentrySemanticAttributes.BREADCRUMB_EVENT_ID]: 'test-event-id', + [InternalSentrySemanticAttributes.BREADCRUMB_CATEGORY]: 'test-category', }, now + 3000, ); span.addEvent( 'breadcrumb event invalid JSON data', { - [OTEL_ATTR_BREADCRUMB_DATA]: 'this is not JSON...', + [InternalSentrySemanticAttributes.BREADCRUMB_DATA]: 'this is not JSON...', }, now + 2000, ); diff --git a/packages/node-experimental/test/sdk/transaction.test.ts b/packages/opentelemetry/test/custom/transaction.test.ts similarity index 77% rename from packages/node-experimental/test/sdk/transaction.test.ts rename to packages/opentelemetry/test/custom/transaction.test.ts index 132696655b09..65f5f79a87eb 100644 --- a/packages/node-experimental/test/sdk/transaction.test.ts +++ b/packages/opentelemetry/test/custom/transaction.test.ts @@ -1,8 +1,7 @@ -import { NodeExperimentalClient } from '../../src/sdk/client'; -import { getCurrentHub } from '../../src/sdk/hub'; -import { NodeExperimentalScope } from '../../src/sdk/scope'; -import { NodeExperimentalTransaction, startTransaction } from '../../src/sdk/transaction'; -import { getDefaultNodeExperimentalClientOptions } from '../helpers/getDefaultNodePreviewClientOptions'; +import { getCurrentHub } from '../../src/custom/hub'; +import { OpenTelemetryScope } from '../../src/custom/scope'; +import { OpenTelemetryTransaction, startTransaction } from '../../src/custom/transaction'; +import { getDefaultTestClientOptions, TestClient } from '../helpers/TestClient'; describe('NodeExperimentalTransaction', () => { afterEach(() => { @@ -10,14 +9,14 @@ describe('NodeExperimentalTransaction', () => { }); it('works with finishWithScope without arguments', () => { - const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions()); + const client = new TestClient(getDefaultTestClientOptions()); const mockSend = jest.spyOn(client, 'captureEvent').mockImplementation(() => 'mocked'); const hub = getCurrentHub(); hub.bindClient(client); - const transaction = new NodeExperimentalTransaction({ name: 'test' }, hub); + const transaction = new OpenTelemetryTransaction({ name: 'test' }, hub); transaction.sampled = true; const res = transaction.finishWithScope(); @@ -56,14 +55,14 @@ describe('NodeExperimentalTransaction', () => { }); it('works with finishWithScope with endTime', () => { - const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions()); + const client = new TestClient(getDefaultTestClientOptions()); const mockSend = jest.spyOn(client, 'captureEvent').mockImplementation(() => 'mocked'); const hub = getCurrentHub(); hub.bindClient(client); - const transaction = new NodeExperimentalTransaction({ name: 'test', startTimestamp: 123456 }, hub); + const transaction = new OpenTelemetryTransaction({ name: 'test', startTimestamp: 123456 }, hub); transaction.sampled = true; const res = transaction.finishWithScope(1234567); @@ -81,17 +80,17 @@ describe('NodeExperimentalTransaction', () => { }); it('works with finishWithScope with endTime & scope', () => { - const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions()); + const client = new TestClient(getDefaultTestClientOptions()); const mockSend = jest.spyOn(client, 'captureEvent').mockImplementation(() => 'mocked'); const hub = getCurrentHub(); hub.bindClient(client); - const transaction = new NodeExperimentalTransaction({ name: 'test', startTimestamp: 123456 }, hub); + const transaction = new OpenTelemetryTransaction({ name: 'test', startTimestamp: 123456 }, hub); transaction.sampled = true; - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); scope.setTags({ tag1: 'yes', tag2: 'no', @@ -140,13 +139,13 @@ describe('startTranscation', () => { }); it('creates a NodeExperimentalTransaction', () => { - const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions({ tracesSampleRate: 0 })); + const client = new TestClient(getDefaultTestClientOptions()); const hub = getCurrentHub(); hub.bindClient(client); const transaction = startTransaction(hub, { name: 'test' }); - expect(transaction).toBeInstanceOf(NodeExperimentalTransaction); + expect(transaction).toBeInstanceOf(OpenTelemetryTransaction); expect(transaction.sampled).toBe(undefined); expect(transaction.spanRecorder).toBeDefined(); @@ -167,7 +166,7 @@ describe('startTranscation', () => { }); it('allows to pass data to transaction', () => { - const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions()); + const client = new TestClient(getDefaultTestClientOptions()); const hub = getCurrentHub(); hub.bindClient(client); @@ -178,7 +177,7 @@ describe('startTranscation', () => { traceId: 'trace1', }); - expect(transaction).toBeInstanceOf(NodeExperimentalTransaction); + expect(transaction).toBeInstanceOf(OpenTelemetryTransaction); expect(transaction.metadata).toEqual({ source: 'custom', diff --git a/packages/opentelemetry/test/helpers/TestClient.ts b/packages/opentelemetry/test/helpers/TestClient.ts new file mode 100644 index 000000000000..d2ed75175b8e --- /dev/null +++ b/packages/opentelemetry/test/helpers/TestClient.ts @@ -0,0 +1,49 @@ +import { BaseClient, createTransport, initAndBind } from '@sentry/core'; +import type { Client, ClientOptions, Event, Options, SeverityLevel } from '@sentry/types'; +import { resolvedSyncPromise } from '@sentry/utils'; + +import { wrapClientClass } from '../../src/custom/client'; +import type { OpenTelemetryClient } from '../../src/types'; + +class BaseTestClient extends BaseClient { + public constructor(options: ClientOptions) { + super(options); + } + + public eventFromException(exception: any): PromiseLike { + return resolvedSyncPromise({ + exception: { + values: [ + { + /* eslint-disable @typescript-eslint/no-unsafe-member-access */ + type: exception.name, + value: exception.message, + /* eslint-enable @typescript-eslint/no-unsafe-member-access */ + }, + ], + }, + }); + } + + public eventFromMessage(message: string, level: SeverityLevel = 'info'): PromiseLike { + return resolvedSyncPromise({ message, level }); + } +} + +export const TestClient = wrapClientClass(BaseTestClient); + +export type TestClientInterface = Client & OpenTelemetryClient; + +export function init(options: Partial = {}): void { + initAndBind(TestClient, getDefaultTestClientOptions(options)); +} + +export function getDefaultTestClientOptions(options: Partial = {}): ClientOptions { + return { + enableTracing: true, + integrations: [], + transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => resolvedSyncPromise({})), + stackParser: () => [], + ...options, + } as ClientOptions; +} diff --git a/packages/opentelemetry/test/helpers/createSpan.ts b/packages/opentelemetry/test/helpers/createSpan.ts new file mode 100644 index 000000000000..38c4ed96f3a8 --- /dev/null +++ b/packages/opentelemetry/test/helpers/createSpan.ts @@ -0,0 +1,30 @@ +import type { Context, SpanContext } from '@opentelemetry/api'; +import { SpanKind } from '@opentelemetry/api'; +import type { Tracer } from '@opentelemetry/sdk-trace-base'; +import { Span } from '@opentelemetry/sdk-trace-base'; +import { uuid4 } from '@sentry/utils'; + +export function createSpan( + name?: string, + { spanId, parentSpanId }: { spanId?: string; parentSpanId?: string } = {}, +): Span { + const spanProcessor = { + onStart: () => {}, + onEnd: () => {}, + }; + const tracer = { + resource: 'test-resource', + instrumentationLibrary: 'test-instrumentation-library', + getSpanLimits: () => ({}), + getActiveSpanProcessor: () => spanProcessor, + } as unknown as Tracer; + + const spanContext: SpanContext = { + spanId: spanId || uuid4(), + traceId: uuid4(), + traceFlags: 0, + }; + + // eslint-disable-next-line deprecation/deprecation + return new Span(tracer, {} as Context, name || 'test', spanContext, SpanKind.INTERNAL, parentSpanId); +} diff --git a/packages/opentelemetry/test/helpers/initOtel.ts b/packages/opentelemetry/test/helpers/initOtel.ts new file mode 100644 index 000000000000..91be948b2f9d --- /dev/null +++ b/packages/opentelemetry/test/helpers/initOtel.ts @@ -0,0 +1,72 @@ +import { diag, DiagLogLevel } from '@opentelemetry/api'; +import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; +import { Resource } from '@opentelemetry/resources'; +import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; +import { SDK_VERSION } from '@sentry/core'; +import { logger } from '@sentry/utils'; + +import { wrapContextManagerClass } from '../../src/contextManager'; +import { getCurrentHub } from '../../src/custom/hub'; +import { SentryPropagator } from '../../src/propagator'; +import { SentrySampler } from '../../src/sampler'; +import { setupEventContextTrace } from '../../src/setupEventContextTrace'; +import { SentrySpanProcessor } from '../../src/spanProcessor'; +import type { TestClientInterface } from './TestClient'; + +/** + * Initialize OpenTelemetry for Node. + */ +export function initOtel(): void { + const client = getCurrentHub().getClient(); + + if (!client) { + __DEBUG_BUILD__ && + logger.warn( + 'No client available, skipping OpenTelemetry setup. This probably means that `Sentry.init()` was not called before `initOtel()`.', + ); + return; + } + + if (client.getOptions().debug) { + const otelLogger = new Proxy(logger as typeof logger & { verbose: (typeof logger)['debug'] }, { + get(target, prop, receiver) { + const actualProp = prop === 'verbose' ? 'debug' : prop; + return Reflect.get(target, actualProp, receiver); + }, + }); + + diag.setLogger(otelLogger, DiagLogLevel.DEBUG); + } + + setupEventContextTrace(client); + + const provider = setupOtel(client); + client.traceProvider = provider; +} + +/** Just exported for tests. */ +export function setupOtel(client: TestClientInterface): BasicTracerProvider { + // Create and configure NodeTracerProvider + const provider = new BasicTracerProvider({ + sampler: new SentrySampler(client), + resource: new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: 'opentelemetry-test', + [SemanticResourceAttributes.SERVICE_NAMESPACE]: 'sentry', + [SemanticResourceAttributes.SERVICE_VERSION]: SDK_VERSION, + }), + forceFlushTimeoutMillis: 500, + }); + provider.addSpanProcessor(new SentrySpanProcessor()); + + // We use a custom context manager to keep context in sync with sentry scope + const SentryContextManager = wrapContextManagerClass(AsyncLocalStorageContextManager); + + // Initialize the provider + provider.register({ + propagator: new SentryPropagator(), + contextManager: new SentryContextManager(), + }); + + return provider; +} diff --git a/packages/opentelemetry/test/helpers/mockSdkInit.ts b/packages/opentelemetry/test/helpers/mockSdkInit.ts new file mode 100644 index 000000000000..50dea300a4a9 --- /dev/null +++ b/packages/opentelemetry/test/helpers/mockSdkInit.ts @@ -0,0 +1,68 @@ +import { context, propagation, ProxyTracerProvider, trace } from '@opentelemetry/api'; +import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; +import type { ClientOptions, Options } from '@sentry/types'; +import { GLOBAL_OBJ } from '@sentry/utils'; + +import { setOpenTelemetryContextAsyncContextStrategy } from '../../src/asyncContextStrategy'; +import { setupGlobalHub } from '../../src/custom/hub'; +import { initOtel } from './initOtel'; +import { init as initTestClient } from './TestClient'; + +const PUBLIC_DSN = 'https://username@domain/123'; + +/** + * Initialize Sentry for Node. + */ +function init(options: Partial | undefined = {}): void { + setupGlobalHub(); + + const fullOptions: Partial = { + instrumenter: 'otel', + ...options, + }; + + initTestClient(fullOptions); + initOtel(); + setOpenTelemetryContextAsyncContextStrategy(); +} + +export function mockSdkInit(options?: Partial) { + GLOBAL_OBJ.__SENTRY__ = { + extensions: {}, + hub: undefined, + globalEventProcessors: [], + logger: undefined, + }; + + init({ dsn: PUBLIC_DSN, ...options }); +} + +export function cleanupOtel(_provider?: BasicTracerProvider): void { + const provider = getProvider(_provider); + + if (!provider) { + return; + } + + void provider.forceFlush(); + void provider.shutdown(); + + // Disable all globally registered APIs + trace.disable(); + context.disable(); + propagation.disable(); +} + +export function getProvider(_provider?: BasicTracerProvider): BasicTracerProvider | undefined { + let provider = _provider || trace.getTracerProvider(); + + if (provider instanceof ProxyTracerProvider) { + provider = provider.getDelegate(); + } + + if (!(provider instanceof BasicTracerProvider)) { + return undefined; + } + + return provider; +} diff --git a/packages/opentelemetry/test/integration/breadcrumbs.test.ts b/packages/opentelemetry/test/integration/breadcrumbs.test.ts new file mode 100644 index 000000000000..f56821a83e2a --- /dev/null +++ b/packages/opentelemetry/test/integration/breadcrumbs.test.ts @@ -0,0 +1,361 @@ +import { withScope } from '@sentry/core'; + +import { getCurrentHub, OpenTelemetryHub } from '../../src/custom/hub'; +import { startSpan } from '../../src/trace'; +import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; +import type { TestClientInterface } from '../helpers/TestClient'; + +describe('Integration | breadcrumbs', () => { + const beforeSendTransaction = jest.fn(() => null); + + afterEach(() => { + cleanupOtel(); + }); + + describe('without tracing', () => { + it('correctly adds & retrieves breadcrumbs', async () => { + const beforeSend = jest.fn(() => null); + const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); + + mockSdkInit({ beforeSend, beforeBreadcrumb }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + + expect(hub).toBeInstanceOf(OpenTelemetryHub); + + hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + hub.addBreadcrumb({ timestamp: 123457, message: 'test2', data: { nested: 'yes' } }); + hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); + + const error = new Error('test'); + hub.captureException(error); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeBreadcrumb).toHaveBeenCalledTimes(3); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test1', timestamp: 123456 }, + { data: { nested: 'yes' }, message: 'test2', timestamp: 123457 }, + { message: 'test3', timestamp: 123455 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('handles parallel scopes', async () => { + const beforeSend = jest.fn(() => null); + const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); + + mockSdkInit({ beforeSend, beforeBreadcrumb }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + + expect(hub).toBeInstanceOf(OpenTelemetryHub); + + const error = new Error('test'); + + hub.addBreadcrumb({ timestamp: 123456, message: 'test0' }); + + withScope(() => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + }); + + withScope(() => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test2' }); + hub.captureException(error); + }); + + withScope(() => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test3' }); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeBreadcrumb).toHaveBeenCalledTimes(4); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test0', timestamp: 123456 }, + { message: 'test2', timestamp: 123456 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + }); + + it('correctly adds & retrieves breadcrumbs', async () => { + const beforeSend = jest.fn(() => null); + const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); + + mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + + const error = new Error('test'); + + startSpan({ name: 'test' }, () => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + + startSpan({ name: 'inner1' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test2', data: { nested: 'yes' } }); + }); + + startSpan({ name: 'inner2' }, () => { + hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); + }); + + hub.captureException(error); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeBreadcrumb).toHaveBeenCalledTimes(3); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test1', timestamp: 123456 }, + { data: { nested: 'yes' }, message: 'test2', timestamp: 123457 }, + { message: 'test3', timestamp: 123455 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('correctly adds & retrieves breadcrumbs for the current root span only', async () => { + const beforeSend = jest.fn(() => null); + const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); + + mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + + const error = new Error('test'); + + startSpan({ name: 'test1' }, () => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test1-a' }); + + startSpan({ name: 'inner1' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test1-b' }); + }); + }); + + startSpan({ name: 'test2' }, () => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test2-a' }); + + startSpan({ name: 'inner2' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); + }); + + hub.captureException(error); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeBreadcrumb).toHaveBeenCalledTimes(4); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test2-a', timestamp: 123456 }, + { message: 'test2-b', timestamp: 123457 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('ignores scopes inside of root span', async () => { + const beforeSend = jest.fn(() => null); + const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); + + mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + + const error = new Error('test'); + + startSpan({ name: 'test1' }, () => { + withScope(() => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + }); + startSpan({ name: 'inner1' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test2' }); + }); + + hub.captureException(error); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeBreadcrumb).toHaveBeenCalledTimes(2); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test1', timestamp: 123456 }, + { message: 'test2', timestamp: 123457 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('handles deep nesting of scopes', async () => { + const beforeSend = jest.fn(() => null); + const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); + + mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + + const error = new Error('test'); + + startSpan({ name: 'test1' }, () => { + withScope(() => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + }); + startSpan({ name: 'inner1' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test2' }); + + startSpan({ name: 'inner2' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test3' }); + + startSpan({ name: 'inner3' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test4' }); + + hub.captureException(error); + + startSpan({ name: 'inner4' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test5' }); + }); + + hub.addBreadcrumb({ timestamp: 123457, message: 'test6' }); + }); + }); + }); + + hub.addBreadcrumb({ timestamp: 123456, message: 'test99' }); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test1', timestamp: 123456 }, + { message: 'test2', timestamp: 123457 }, + { message: 'test3', timestamp: 123457 }, + { message: 'test4', timestamp: 123457 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('correctly adds & retrieves breadcrumbs in async spans', async () => { + const beforeSend = jest.fn(() => null); + const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); + + mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + + const error = new Error('test'); + + const promise1 = startSpan({ name: 'test' }, async () => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + + await startSpan({ name: 'inner1' }, async () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test2' }); + }); + + await startSpan({ name: 'inner2' }, async () => { + hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); + }); + + await new Promise(resolve => setTimeout(resolve, 10)); + + hub.captureException(error); + }); + + const promise2 = startSpan({ name: 'test-b' }, async () => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test1-b' }); + + await startSpan({ name: 'inner1' }, async () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); + }); + + await startSpan({ name: 'inner2' }, async () => { + hub.addBreadcrumb({ timestamp: 123455, message: 'test3-b' }); + }); + }); + + await Promise.all([promise1, promise2]); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeBreadcrumb).toHaveBeenCalledTimes(6); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test1', timestamp: 123456 }, + { message: 'test2', timestamp: 123457 }, + { message: 'test3', timestamp: 123455 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); +}); diff --git a/packages/node-experimental/test/integration/otelTimedEvents.test.ts b/packages/opentelemetry/test/integration/otelTimedEvents.test.ts similarity index 76% rename from packages/node-experimental/test/integration/otelTimedEvents.test.ts rename to packages/opentelemetry/test/integration/otelTimedEvents.test.ts index 8bdaec750a15..0fb1f1ff9d26 100644 --- a/packages/node-experimental/test/integration/otelTimedEvents.test.ts +++ b/packages/opentelemetry/test/integration/otelTimedEvents.test.ts @@ -1,9 +1,9 @@ import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import type { NodeExperimentalClient } from '../../src/sdk/client'; -import { getCurrentHub } from '../../src/sdk/hub'; -import { startSpan } from '../../src/sdk/trace'; +import { getCurrentHub } from '../../src/custom/hub'; +import { startSpan } from '../../src/trace'; import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; +import type { TestClientInterface } from '../helpers/TestClient'; describe('Integration | OTEL TimedEvents', () => { afterEach(() => { @@ -17,15 +17,15 @@ describe('Integration | OTEL TimedEvents', () => { mockSdkInit({ beforeSend, beforeSendTransaction, enableTracing: true }); const hub = getCurrentHub(); - const client = hub.getClient() as NodeExperimentalClient; + const client = hub.getClient() as TestClientInterface; startSpan({ name: 'test' }, span => { - span?.addEvent('exception', { + span.addEvent('exception', { [SemanticAttributes.EXCEPTION_MESSAGE]: 'test-message', 'test-span-event-attr': 'test-span-event-attr-value', }); - span?.addEvent('other', { + span.addEvent('other', { [SemanticAttributes.EXCEPTION_MESSAGE]: 'test-message-2', 'test-span-event-attr': 'test-span-event-attr-value', }); @@ -39,8 +39,6 @@ describe('Integration | OTEL TimedEvents', () => { exception: { values: [ { - mechanism: { handled: true, type: 'generic' }, - stacktrace: expect.any(Object), type: 'Error', value: 'test-message', }, diff --git a/packages/opentelemetry/test/integration/scope.test.ts b/packages/opentelemetry/test/integration/scope.test.ts new file mode 100644 index 000000000000..c028e1893d7a --- /dev/null +++ b/packages/opentelemetry/test/integration/scope.test.ts @@ -0,0 +1,238 @@ +import { captureException, setTag, withScope } from '@sentry/core'; + +import { getCurrentHub, OpenTelemetryHub } from '../../src/custom/hub'; +import { OpenTelemetryScope } from '../../src/custom/scope'; +import { startSpan } from '../../src/trace'; +import { getSpanScope } from '../../src/utils/spanData'; +import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; +import type { TestClientInterface } from '../helpers/TestClient'; + +describe('Integration | Scope', () => { + afterEach(() => { + cleanupOtel(); + }); + + describe.each([ + ['with tracing', true], + ['without tracing', false], + ])('%s', (_name, enableTracing) => { + it('correctly syncs OTEL context & Sentry hub/scope', async () => { + const beforeSend = jest.fn(() => null); + const beforeSendTransaction = jest.fn(() => null); + + mockSdkInit({ enableTracing, beforeSend, beforeSendTransaction }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + + const rootScope = hub.getScope(); + + expect(hub).toBeInstanceOf(OpenTelemetryHub); + expect(rootScope).toBeInstanceOf(OpenTelemetryScope); + + const error = new Error('test error'); + let spanId: string | undefined; + let traceId: string | undefined; + + rootScope.setTag('tag1', 'val1'); + + withScope(scope1 => { + scope1.setTag('tag2', 'val2'); + + withScope(scope2b => { + scope2b.setTag('tag3-b', 'val3-b'); + }); + + withScope(scope2 => { + scope2.setTag('tag3', 'val3'); + + startSpan({ name: 'outer' }, span => { + expect(getSpanScope(span)).toBe(enableTracing ? scope2 : undefined); + + spanId = span.spanContext().spanId; + traceId = span.spanContext().traceId; + + setTag('tag4', 'val4'); + + captureException(error); + }); + }); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: spanId + ? { + span_id: spanId, + trace_id: traceId, + parent_span_id: undefined, + } + : expect.any(Object), + }), + tags: { + tag1: 'val1', + tag2: 'val2', + tag3: 'val3', + tag4: 'val4', + }, + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + + if (enableTracing) { + expect(beforeSendTransaction).toHaveBeenCalledTimes(1); + // Note: Scope for transaction is taken at `start` time, not `finish` time + expect(beforeSendTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: { + data: { 'otel.kind': 'INTERNAL' }, + span_id: spanId, + status: 'ok', + trace_id: traceId, + }, + }), + + spans: [], + start_timestamp: expect.any(Number), + tags: { + tag1: 'val1', + tag2: 'val2', + tag3: 'val3', + }, + timestamp: expect.any(Number), + transaction: 'outer', + transaction_info: { source: 'custom' }, + type: 'transaction', + }), + { + event_id: expect.any(String), + }, + ); + } + }); + + it('isolates parallel root scopes', async () => { + const beforeSend = jest.fn(() => null); + const beforeSendTransaction = jest.fn(() => null); + + mockSdkInit({ enableTracing, beforeSend, beforeSendTransaction }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + + const rootScope = hub.getScope(); + + expect(hub).toBeInstanceOf(OpenTelemetryHub); + expect(rootScope).toBeInstanceOf(OpenTelemetryScope); + + const error1 = new Error('test error 1'); + const error2 = new Error('test error 2'); + let spanId1: string | undefined; + let spanId2: string | undefined; + let traceId1: string | undefined; + let traceId2: string | undefined; + + rootScope.setTag('tag1', 'val1'); + + withScope(scope1 => { + scope1.setTag('tag2', 'val2a'); + + withScope(scope2 => { + scope2.setTag('tag3', 'val3a'); + + startSpan({ name: 'outer' }, span => { + spanId1 = span.spanContext().spanId; + traceId1 = span.spanContext().traceId; + + setTag('tag4', 'val4a'); + + captureException(error1); + }); + }); + }); + + withScope(scope1 => { + scope1.setTag('tag2', 'val2b'); + + withScope(scope2 => { + scope2.setTag('tag3', 'val3b'); + + startSpan({ name: 'outer' }, span => { + spanId2 = span.spanContext().spanId; + traceId2 = span.spanContext().traceId; + + setTag('tag4', 'val4b'); + + captureException(error2); + }); + }); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(2); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: spanId1 + ? { + span_id: spanId1, + trace_id: traceId1, + parent_span_id: undefined, + } + : expect.any(Object), + }), + tags: { + tag1: 'val1', + tag2: 'val2a', + tag3: 'val3a', + tag4: 'val4a', + }, + }), + { + event_id: expect.any(String), + originalException: error1, + syntheticException: expect.any(Error), + }, + ); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: spanId2 + ? { + span_id: spanId2, + trace_id: traceId2, + parent_span_id: undefined, + } + : expect.any(Object), + }), + tags: { + tag1: 'val1', + tag2: 'val2b', + tag3: 'val3b', + tag4: 'val4b', + }, + }), + { + event_id: expect.any(String), + originalException: error2, + syntheticException: expect.any(Error), + }, + ); + + if (enableTracing) { + expect(beforeSendTransaction).toHaveBeenCalledTimes(2); + } + }); + }); +}); diff --git a/packages/opentelemetry/test/integration/transactions.test.ts b/packages/opentelemetry/test/integration/transactions.test.ts new file mode 100644 index 000000000000..2a4de232cc1b --- /dev/null +++ b/packages/opentelemetry/test/integration/transactions.test.ts @@ -0,0 +1,536 @@ +import { context, trace, TraceFlags } from '@opentelemetry/api'; +import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; +import { addBreadcrumb, setTag } from '@sentry/core'; +import type { PropagationContext, TransactionEvent } from '@sentry/types'; +import { logger } from '@sentry/utils'; + +import { getCurrentHub } from '../../src/custom/hub'; +import { SentrySpanProcessor } from '../../src/spanProcessor'; +import { startInactiveSpan, startSpan } from '../../src/trace'; +import { setPropagationContextOnContext } from '../../src/utils/contextData'; +import { cleanupOtel, getProvider, mockSdkInit } from '../helpers/mockSdkInit'; +import type { TestClientInterface } from '../helpers/TestClient'; + +describe('Integration | Transactions', () => { + afterEach(() => { + jest.restoreAllMocks(); + cleanupOtel(); + }); + + it('correctly creates transaction & spans', async () => { + const beforeSendTransaction = jest.fn(() => null); + + mockSdkInit({ enableTracing: true, beforeSendTransaction }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + + addBreadcrumb({ message: 'test breadcrumb 1', timestamp: 123456 }); + setTag('outer.tag', 'test value'); + + startSpan( + { + op: 'test op', + name: 'test name', + source: 'task', + origin: 'auto.test', + metadata: { requestPath: 'test-path' }, + }, + span => { + if (!span) { + return; + } + + addBreadcrumb({ message: 'test breadcrumb 2', timestamp: 123456 }); + + span.setAttributes({ + 'test.outer': 'test value', + }); + + const subSpan = startInactiveSpan({ name: 'inner span 1' }); + subSpan?.end(); + + setTag('test.tag', 'test value'); + + startSpan({ name: 'inner span 2' }, innerSpan => { + if (!innerSpan) { + return; + } + + addBreadcrumb({ message: 'test breadcrumb 3', timestamp: 123456 }); + + innerSpan.setAttributes({ + 'test.inner': 'test value', + }); + }); + }, + ); + + await client.flush(); + + expect(beforeSendTransaction).toHaveBeenCalledTimes(1); + expect(beforeSendTransaction).toHaveBeenLastCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test breadcrumb 1', timestamp: 123456 }, + { message: 'test breadcrumb 2', timestamp: 123456 }, + { message: 'test breadcrumb 3', timestamp: 123456 }, + ], + contexts: { + otel: { + attributes: { + 'test.outer': 'test value', + }, + resource: { + 'service.name': 'opentelemetry-test', + 'service.namespace': 'sentry', + 'service.version': expect.any(String), + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': expect.any(String), + }, + }, + trace: { + data: { 'otel.kind': 'INTERNAL' }, + op: 'test op', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + }, + }, + environment: 'production', + event_id: expect.any(String), + sdkProcessingMetadata: expect.objectContaining({ + dynamicSamplingContext: expect.objectContaining({ + environment: 'production', + public_key: expect.any(String), + sample_rate: '1', + sampled: 'true', + trace_id: expect.any(String), + transaction: 'test name', + }), + propagationContext: { + sampled: undefined, + spanId: expect.any(String), + traceId: expect.any(String), + }, + sampleRate: 1, + source: 'task', + spanMetadata: expect.any(Object), + requestPath: 'test-path', + }), + // spans are circular (they have a reference to the transaction), which leads to jest choking on this + // instead we compare them in detail below + spans: [ + expect.objectContaining({ + description: 'inner span 1', + }), + expect.objectContaining({ + description: 'inner span 2', + }), + ], + start_timestamp: expect.any(Number), + tags: { + 'outer.tag': 'test value', + }, + timestamp: expect.any(Number), + transaction: 'test name', + transaction_info: { source: 'task' }, + type: 'transaction', + }), + { + event_id: expect.any(String), + }, + ); + + // Checking the spans here, as they are circular to the transaction... + const runArgs = beforeSendTransaction.mock.calls[0] as unknown as [TransactionEvent, unknown]; + const spans = runArgs[0].spans || []; + + // note: Currently, spans do not have any context/span added to them + // This is the same behavior as for the "regular" SDKs + expect(spans.map(span => span.toJSON())).toEqual([ + { + data: { 'otel.kind': 'INTERNAL' }, + description: 'inner span 1', + origin: 'manual', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }, + { + data: { 'otel.kind': 'INTERNAL', 'test.inner': 'test value' }, + description: 'inner span 2', + origin: 'manual', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }, + ]); + }); + + it('correctly creates concurrent transaction & spans', async () => { + const beforeSendTransaction = jest.fn(() => null); + + mockSdkInit({ enableTracing: true, beforeSendTransaction }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + + addBreadcrumb({ message: 'test breadcrumb 1', timestamp: 123456 }); + + startSpan({ op: 'test op', name: 'test name', source: 'task', origin: 'auto.test' }, span => { + if (!span) { + return; + } + + addBreadcrumb({ message: 'test breadcrumb 2', timestamp: 123456 }); + + span.setAttributes({ + 'test.outer': 'test value', + }); + + const subSpan = startInactiveSpan({ name: 'inner span 1' }); + subSpan?.end(); + + setTag('test.tag', 'test value'); + + startSpan({ name: 'inner span 2' }, innerSpan => { + if (!innerSpan) { + return; + } + + addBreadcrumb({ message: 'test breadcrumb 3', timestamp: 123456 }); + + innerSpan.setAttributes({ + 'test.inner': 'test value', + }); + }); + }); + + startSpan({ op: 'test op b', name: 'test name b' }, span => { + if (!span) { + return; + } + + addBreadcrumb({ message: 'test breadcrumb 2b', timestamp: 123456 }); + + span.setAttributes({ + 'test.outer': 'test value b', + }); + + const subSpan = startInactiveSpan({ name: 'inner span 1b' }); + subSpan?.end(); + + setTag('test.tag', 'test value b'); + + startSpan({ name: 'inner span 2b' }, innerSpan => { + if (!innerSpan) { + return; + } + + addBreadcrumb({ message: 'test breadcrumb 3b', timestamp: 123456 }); + + innerSpan.setAttributes({ + 'test.inner': 'test value b', + }); + }); + }); + + await client.flush(); + + expect(beforeSendTransaction).toHaveBeenCalledTimes(2); + expect(beforeSendTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test breadcrumb 1', timestamp: 123456 }, + { message: 'test breadcrumb 2', timestamp: 123456 }, + { message: 'test breadcrumb 3', timestamp: 123456 }, + ], + contexts: expect.objectContaining({ + otel: expect.objectContaining({ + attributes: { + 'test.outer': 'test value', + }, + }), + trace: { + data: { 'otel.kind': 'INTERNAL' }, + op: 'test op', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + }, + }), + spans: [ + expect.objectContaining({ + description: 'inner span 1', + }), + expect.objectContaining({ + description: 'inner span 2', + }), + ], + start_timestamp: expect.any(Number), + tags: {}, + timestamp: expect.any(Number), + transaction: 'test name', + transaction_info: { source: 'task' }, + type: 'transaction', + }), + { + event_id: expect.any(String), + }, + ); + + expect(beforeSendTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test breadcrumb 1', timestamp: 123456 }, + { message: 'test breadcrumb 2b', timestamp: 123456 }, + { message: 'test breadcrumb 3b', timestamp: 123456 }, + ], + contexts: expect.objectContaining({ + otel: expect.objectContaining({ + attributes: { + 'test.outer': 'test value b', + }, + }), + trace: { + data: { 'otel.kind': 'INTERNAL' }, + op: 'test op b', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + }, + }), + spans: [ + expect.objectContaining({ + description: 'inner span 1b', + }), + expect.objectContaining({ + description: 'inner span 2b', + }), + ], + start_timestamp: expect.any(Number), + tags: {}, + timestamp: expect.any(Number), + transaction: 'test name b', + transaction_info: { source: 'custom' }, + type: 'transaction', + }), + { + event_id: expect.any(String), + }, + ); + }); + + it('correctly creates transaction & spans with a trace header data', async () => { + const beforeSendTransaction = jest.fn(() => null); + + const traceId = 'd4cda95b652f4a1592b449d5929fda1b'; + const parentSpanId = '6e0c63257de34c92'; + + const spanContext = { + traceId, + spanId: parentSpanId, + sampled: true, + isRemote: true, + traceFlags: TraceFlags.SAMPLED, + }; + + const propagationContext: PropagationContext = { + traceId, + parentSpanId, + spanId: '6e0c63257de34c93', + sampled: true, + }; + + mockSdkInit({ enableTracing: true, beforeSendTransaction }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + + // We simulate the correct context we'd normally get from the SentryPropagator + context.with( + trace.setSpanContext(setPropagationContextOnContext(context.active(), propagationContext), spanContext), + () => { + startSpan({ op: 'test op', name: 'test name', source: 'task', origin: 'auto.test' }, span => { + if (!span) { + return; + } + + const subSpan = startInactiveSpan({ name: 'inner span 1' }); + subSpan?.end(); + + startSpan({ name: 'inner span 2' }, innerSpan => { + if (!innerSpan) { + return; + } + }); + }); + }, + ); + + await client.flush(); + + expect(beforeSendTransaction).toHaveBeenCalledTimes(1); + expect(beforeSendTransaction).toHaveBeenLastCalledWith( + expect.objectContaining({ + contexts: expect.objectContaining({ + otel: expect.objectContaining({ + attributes: {}, + }), + trace: { + data: { 'otel.kind': 'INTERNAL' }, + op: 'test op', + span_id: expect.any(String), + parent_span_id: parentSpanId, + status: 'ok', + trace_id: traceId, + }, + }), + // spans are circular (they have a reference to the transaction), which leads to jest choking on this + // instead we compare them in detail below + spans: [ + expect.objectContaining({ + description: 'inner span 1', + }), + expect.objectContaining({ + description: 'inner span 2', + }), + ], + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: 'test name', + transaction_info: { source: 'task' }, + type: 'transaction', + }), + { + event_id: expect.any(String), + }, + ); + + // Checking the spans here, as they are circular to the transaction... + const runArgs = beforeSendTransaction.mock.calls[0] as unknown as [TransactionEvent, unknown]; + const spans = runArgs[0].spans || []; + + // note: Currently, spans do not have any context/span added to them + // This is the same behavior as for the "regular" SDKs + expect(spans.map(span => span.toJSON())).toEqual([ + { + data: { 'otel.kind': 'INTERNAL' }, + description: 'inner span 1', + origin: 'manual', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: traceId, + }, + { + data: { 'otel.kind': 'INTERNAL' }, + description: 'inner span 2', + origin: 'manual', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: traceId, + }, + ]); + }); + + it('cleans up spans that are not flushed for over 5 mins', async () => { + const beforeSendTransaction = jest.fn(() => null); + + const now = Date.now(); + jest.useFakeTimers(); + jest.setSystemTime(now); + + const logs: unknown[] = []; + jest.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg)); + + mockSdkInit({ enableTracing: true, beforeSendTransaction }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + const provider = getProvider(); + const multiSpanProcessor = provider?.activeSpanProcessor as + | (SpanProcessor & { _spanProcessors?: SpanProcessor[] }) + | undefined; + const spanProcessor = multiSpanProcessor?.['_spanProcessors']?.find( + spanProcessor => spanProcessor instanceof SentrySpanProcessor, + ) as SentrySpanProcessor | undefined; + + const exporter = spanProcessor ? spanProcessor['_exporter'] : undefined; + + if (!exporter) { + throw new Error('No exporter found, aborting test...'); + } + + let innerSpan1Id: string | undefined; + let innerSpan2Id: string | undefined; + + void startSpan({ name: 'test name' }, async span => { + if (!span) { + return; + } + + const subSpan = startInactiveSpan({ name: 'inner span 1' }); + innerSpan1Id = subSpan?.spanContext().spanId; + subSpan?.end(); + + startSpan({ name: 'inner span 2' }, innerSpan => { + if (!innerSpan) { + return; + } + + innerSpan2Id = innerSpan.spanContext().spanId; + }); + + // Pretend this is pending for 10 minutes + await new Promise(resolve => setTimeout(resolve, 10 * 60 * 1000)); + }); + + // Nothing added to exporter yet + expect(exporter['_finishedSpans'].length).toBe(0); + + void client.flush(5_000); + jest.advanceTimersByTime(5_000); + + // Now the child-spans have been added to the exporter, but they are pending since they are waiting for their parant + expect(exporter['_finishedSpans'].length).toBe(2); + expect(beforeSendTransaction).toHaveBeenCalledTimes(0); + + // Now wait for 5 mins + jest.advanceTimersByTime(5 * 60 * 1_000); + + // Adding another span will trigger the cleanup + startSpan({ name: 'other span' }, () => {}); + + void client.flush(5_000); + jest.advanceTimersByTime(5_000); + + // Old spans have been cleared away + expect(exporter['_finishedSpans'].length).toBe(0); + + // Called once for the 'other span' + expect(beforeSendTransaction).toHaveBeenCalledTimes(1); + + expect(logs).toEqual( + expect.arrayContaining([ + 'SpanExporter exported 0 spans, 2 unsent spans remaining', + 'SpanExporter exported 1 spans, 2 unsent spans remaining', + `SpanExporter dropping span inner span 1 (${innerSpan1Id}) because it is pending for more than 5 minutes.`, + `SpanExporter dropping span inner span 2 (${innerSpan2Id}) because it is pending for more than 5 minutes.`, + ]), + ); + }); +}); diff --git a/packages/node-experimental/test/opentelemetry/propagator.test.ts b/packages/opentelemetry/test/propagator.test.ts similarity index 90% rename from packages/node-experimental/test/opentelemetry/propagator.test.ts rename to packages/opentelemetry/test/propagator.test.ts index 80b027496428..c90a2636a58a 100644 --- a/packages/node-experimental/test/opentelemetry/propagator.test.ts +++ b/packages/opentelemetry/test/propagator.test.ts @@ -1,4 +1,3 @@ -import type { Context } from '@opentelemetry/api'; import { defaultTextMapGetter, defaultTextMapSetter, @@ -11,12 +10,9 @@ import { suppressTracing } from '@opentelemetry/core'; import { addTracingExtensions, Hub, makeMain } from '@sentry/core'; import type { PropagationContext } from '@sentry/types'; -import { - SENTRY_BAGGAGE_HEADER, - SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, - SENTRY_TRACE_HEADER, -} from '../../src/constants'; -import { SentryPropagator } from '../../src/opentelemetry/propagator'; +import { SENTRY_BAGGAGE_HEADER, SENTRY_TRACE_HEADER } from '../src/constants'; +import { SentryPropagator } from '../src/propagator'; +import { getPropagationContextFromContext, setPropagationContextOnContext } from '../src/utils/contextData'; beforeAll(() => { addTracingExtensions(); @@ -167,7 +163,10 @@ describe('SentryPropagator', () => { 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c94-1', ], ])('%s', (_name, spanContext, propagationContext, baggage, sentryTrace) => { - const context = trace.setSpanContext(setPropagationContext(ROOT_CONTEXT, propagationContext), spanContext); + const context = trace.setSpanContext( + setPropagationContextOnContext(ROOT_CONTEXT, propagationContext), + spanContext, + ); propagator.inject(context, carrier, defaultTextMapSetter); expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual(baggage.sort()); expect(carrier[SENTRY_TRACE_HEADER]).toBe(sentryTrace); @@ -194,7 +193,10 @@ describe('SentryPropagator', () => { spanId: '6e0c63257de34c92', traceFlags: TraceFlags.SAMPLED, }; - const context = trace.setSpanContext(setPropagationContext(ROOT_CONTEXT, propagationContext), spanContext); + const context = trace.setSpanContext( + setPropagationContextOnContext(ROOT_CONTEXT, propagationContext), + spanContext, + ); const baggage = propagation.createBaggage({ foo: { value: 'bar' } }); propagator.inject(propagation.setBaggage(context, baggage), carrier, defaultTextMapSetter); expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual( @@ -243,7 +245,7 @@ describe('SentryPropagator', () => { }, }; const context = suppressTracing( - trace.setSpanContext(setPropagationContext(ROOT_CONTEXT, propagationContext), spanContext), + trace.setSpanContext(setPropagationContextOnContext(ROOT_CONTEXT, propagationContext), spanContext), ); propagator.inject(context, carrier, defaultTextMapSetter); expect(carrier[SENTRY_TRACE_HEADER]).toBe(undefined); @@ -267,7 +269,7 @@ describe('SentryPropagator', () => { }, }; - const context = setPropagationContext(ROOT_CONTEXT, propagationContext); + const context = setPropagationContextOnContext(ROOT_CONTEXT, propagationContext); propagator.inject(context, carrier, defaultTextMapSetter); expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual( [ @@ -301,7 +303,7 @@ describe('SentryPropagator', () => { carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader; const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); - const propagationContext = context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY) as PropagationContext; + const propagationContext = getPropagationContextFromContext(context); expect(propagationContext).toEqual({ sampled: true, parentSpanId: '6e0c63257de34c92', @@ -310,14 +312,14 @@ describe('SentryPropagator', () => { }); // Ensure spanId !== parentSpanId - it should be a new random ID - expect(propagationContext.spanId).not.toBe('6e0c63257de34c92'); + expect(propagationContext?.spanId).not.toBe('6e0c63257de34c92'); }); it('sets undefined sentry trace header on context', () => { const sentryTraceHeader = undefined; carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader; const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); - expect(context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY)).toEqual({ + expect(getPropagationContextFromContext(context)).toEqual({ sampled: undefined, spanId: expect.any(String), traceId: expect.any(String), @@ -329,7 +331,7 @@ describe('SentryPropagator', () => { 'sentry-environment=production,sentry-release=1.0.0,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-transaction=dsc-transaction'; carrier[SENTRY_BAGGAGE_HEADER] = baggage; const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); - expect(context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY)).toEqual({ + expect(getPropagationContextFromContext(context)).toEqual({ sampled: undefined, spanId: expect.any(String), traceId: expect.any(String), // Note: This is not automatically taken from the DSC (in reality, this should be aligned) @@ -347,7 +349,7 @@ describe('SentryPropagator', () => { const baggage = ''; carrier[SENTRY_BAGGAGE_HEADER] = baggage; const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); - expect(context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY)).toEqual({ + expect(getPropagationContextFromContext(context)).toEqual({ sampled: undefined, spanId: expect.any(String), traceId: expect.any(String), @@ -357,7 +359,7 @@ describe('SentryPropagator', () => { it('handles when sentry-trace is an empty array', () => { carrier[SENTRY_TRACE_HEADER] = []; const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); - expect(context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY)).toEqual({ + expect(getPropagationContextFromContext(context)).toEqual({ sampled: undefined, spanId: expect.any(String), traceId: expect.any(String), @@ -366,10 +368,6 @@ describe('SentryPropagator', () => { }); }); -function setPropagationContext(context: Context, propagationContext: PropagationContext): Context { - return context.setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext); -} - function baggageToArray(baggage: unknown): string[] { return typeof baggage === 'string' ? baggage.split(',').sort() : []; } diff --git a/packages/node-experimental/test/sdk/trace.test.ts b/packages/opentelemetry/test/trace.test.ts similarity index 52% rename from packages/node-experimental/test/sdk/trace.test.ts rename to packages/opentelemetry/test/trace.test.ts index e141372552a6..18037c7412af 100644 --- a/packages/node-experimental/test/sdk/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -1,18 +1,17 @@ +import type { Span } from '@opentelemetry/api'; import { context, trace, TraceFlags } from '@opentelemetry/api'; -import type { Span } from '@opentelemetry/sdk-trace-base'; +import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; import type { PropagationContext } from '@sentry/types'; -import * as Sentry from '../../src'; -import { - OTEL_ATTR_OP, - OTEL_ATTR_ORIGIN, - OTEL_ATTR_SENTRY_SAMPLE_RATE, - OTEL_ATTR_SOURCE, - SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, -} from '../../src/constants'; -import { getSpanMetadata } from '../../src/opentelemetry/spanData'; -import { getActiveSpan } from '../../src/utils/getActiveSpan'; -import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; +import { getCurrentHub } from '../src/custom/hub'; +import { InternalSentrySemanticAttributes } from '../src/semanticAttributes'; +import { startInactiveSpan, startSpan } from '../src/trace'; +import type { AbstractSpan } from '../src/types'; +import { setPropagationContextOnContext } from '../src/utils/contextData'; +import { getActiveSpan, getRootSpan } from '../src/utils/getActiveSpan'; +import { getSpanMetadata } from '../src/utils/spanData'; +import { spanHasAttributes, spanHasName } from '../src/utils/spanTypes'; +import { cleanupOtel, mockSdkInit } from './helpers/mockSdkInit'; describe('trace', () => { beforeEach(() => { @@ -29,18 +28,18 @@ describe('trace', () => { expect(getActiveSpan()).toEqual(undefined); - const res = Sentry.startSpan({ name: 'outer' }, outerSpan => { + const res = startSpan({ name: 'outer' }, outerSpan => { expect(outerSpan).toBeDefined(); - spans.push(outerSpan!); + spans.push(outerSpan); - expect(outerSpan?.name).toEqual('outer'); + expect(getSpanName(outerSpan)).toEqual('outer'); expect(getActiveSpan()).toEqual(outerSpan); - Sentry.startSpan({ name: 'inner' }, innerSpan => { + startSpan({ name: 'inner' }, innerSpan => { expect(innerSpan).toBeDefined(); - spans.push(innerSpan!); + spans.push(innerSpan); - expect(innerSpan?.name).toEqual('inner'); + expect(getSpanName(innerSpan)).toEqual('inner'); expect(getActiveSpan()).toEqual(innerSpan); }); @@ -53,11 +52,11 @@ describe('trace', () => { expect(spans).toHaveLength(2); const [outerSpan, innerSpan] = spans; - expect(outerSpan.name).toEqual('outer'); - expect(innerSpan.name).toEqual('inner'); + expect(getSpanName(outerSpan)).toEqual('outer'); + expect(getSpanName(innerSpan)).toEqual('inner'); - expect(outerSpan.endTime).not.toEqual([0, 0]); - expect(innerSpan.endTime).not.toEqual([0, 0]); + expect(getSpanEndTime(outerSpan)).not.toEqual([0, 0]); + expect(getSpanEndTime(innerSpan)).not.toEqual([0, 0]); }); it('works with an async callback', async () => { @@ -65,22 +64,22 @@ describe('trace', () => { expect(getActiveSpan()).toEqual(undefined); - const res = await Sentry.startSpan({ name: 'outer' }, async outerSpan => { + const res = await startSpan({ name: 'outer' }, async outerSpan => { expect(outerSpan).toBeDefined(); - spans.push(outerSpan!); + spans.push(outerSpan); await new Promise(resolve => setTimeout(resolve, 10)); - expect(outerSpan?.name).toEqual('outer'); + expect(getSpanName(outerSpan)).toEqual('outer'); expect(getActiveSpan()).toEqual(outerSpan); - await Sentry.startSpan({ name: 'inner' }, async innerSpan => { + await startSpan({ name: 'inner' }, async innerSpan => { expect(innerSpan).toBeDefined(); - spans.push(innerSpan!); + spans.push(innerSpan); await new Promise(resolve => setTimeout(resolve, 10)); - expect(innerSpan?.name).toEqual('inner'); + expect(getSpanName(innerSpan)).toEqual('inner'); expect(getActiveSpan()).toEqual(innerSpan); }); @@ -93,11 +92,11 @@ describe('trace', () => { expect(spans).toHaveLength(2); const [outerSpan, innerSpan] = spans; - expect(outerSpan.name).toEqual('outer'); - expect(innerSpan.name).toEqual('inner'); + expect(getSpanName(outerSpan)).toEqual('outer'); + expect(getSpanName(innerSpan)).toEqual('inner'); - expect(outerSpan.endTime).not.toEqual([0, 0]); - expect(innerSpan.endTime).not.toEqual([0, 0]); + expect(getSpanEndTime(outerSpan)).not.toEqual([0, 0]); + expect(getSpanEndTime(innerSpan)).not.toEqual([0, 0]); }); it('works with multiple parallel calls', () => { @@ -106,34 +105,34 @@ describe('trace', () => { expect(getActiveSpan()).toEqual(undefined); - Sentry.startSpan({ name: 'outer' }, outerSpan => { + startSpan({ name: 'outer' }, outerSpan => { expect(outerSpan).toBeDefined(); - spans1.push(outerSpan!); + spans1.push(outerSpan); - expect(outerSpan?.name).toEqual('outer'); + expect(getSpanName(outerSpan)).toEqual('outer'); expect(getActiveSpan()).toEqual(outerSpan); - Sentry.startSpan({ name: 'inner' }, innerSpan => { + startSpan({ name: 'inner' }, innerSpan => { expect(innerSpan).toBeDefined(); - spans1.push(innerSpan!); + spans1.push(innerSpan); - expect(innerSpan?.name).toEqual('inner'); + expect(getSpanName(innerSpan)).toEqual('inner'); expect(getActiveSpan()).toEqual(innerSpan); }); }); - Sentry.startSpan({ name: 'outer2' }, outerSpan => { + startSpan({ name: 'outer2' }, outerSpan => { expect(outerSpan).toBeDefined(); - spans2.push(outerSpan!); + spans2.push(outerSpan); - expect(outerSpan?.name).toEqual('outer2'); + expect(getSpanName(outerSpan)).toEqual('outer2'); expect(getActiveSpan()).toEqual(outerSpan); - Sentry.startSpan({ name: 'inner2' }, innerSpan => { + startSpan({ name: 'inner2' }, innerSpan => { expect(innerSpan).toBeDefined(); - spans2.push(innerSpan!); + spans2.push(innerSpan); - expect(innerSpan?.name).toEqual('inner2'); + expect(getSpanName(innerSpan)).toEqual('inner2'); expect(getActiveSpan()).toEqual(innerSpan); }); }); @@ -143,22 +142,75 @@ describe('trace', () => { expect(spans2).toHaveLength(2); }); + it('works with multiple parallel async calls', async () => { + const spans1: Span[] = []; + const spans2: Span[] = []; + + expect(getActiveSpan()).toEqual(undefined); + + const promise1 = startSpan({ name: 'outer' }, async outerSpan => { + expect(outerSpan).toBeDefined(); + spans1.push(outerSpan); + + expect(getSpanName(outerSpan)).toEqual('outer'); + expect(getActiveSpan()).toEqual(outerSpan); + expect(getRootSpan(outerSpan)).toEqual(outerSpan); + + await new Promise(resolve => setTimeout(resolve, 10)); + + await startSpan({ name: 'inner' }, async innerSpan => { + expect(innerSpan).toBeDefined(); + spans1.push(innerSpan); + + expect(getSpanName(innerSpan)).toEqual('inner'); + expect(getActiveSpan()).toEqual(innerSpan); + expect(getRootSpan(innerSpan)).toEqual(outerSpan); + }); + }); + + const promise2 = startSpan({ name: 'outer2' }, async outerSpan => { + expect(outerSpan).toBeDefined(); + spans2.push(outerSpan); + + expect(getSpanName(outerSpan)).toEqual('outer2'); + expect(getActiveSpan()).toEqual(outerSpan); + expect(getRootSpan(outerSpan)).toEqual(outerSpan); + + await new Promise(resolve => setTimeout(resolve, 10)); + + await startSpan({ name: 'inner2' }, async innerSpan => { + expect(innerSpan).toBeDefined(); + spans2.push(innerSpan); + + expect(getSpanName(innerSpan)).toEqual('inner2'); + expect(getActiveSpan()).toEqual(innerSpan); + expect(getRootSpan(innerSpan)).toEqual(outerSpan); + }); + }); + + await Promise.all([promise1, promise2]); + + expect(getActiveSpan()).toEqual(undefined); + expect(spans1).toHaveLength(2); + expect(spans2).toHaveLength(2); + }); + it('allows to pass context arguments', () => { - Sentry.startSpan( + startSpan( { name: 'outer', }, span => { expect(span).toBeDefined(); - expect(span?.attributes).toEqual({ - [OTEL_ATTR_SENTRY_SAMPLE_RATE]: 1, + expect(getSpanAttributes(span)).toEqual({ + [InternalSentrySemanticAttributes.SAMPLE_RATE]: 1, }); - expect(getSpanMetadata(span!)).toEqual(undefined); + expect(getSpanMetadata(span)).toEqual(undefined); }, ); - Sentry.startSpan( + startSpan( { name: 'outer', op: 'my-op', @@ -168,14 +220,14 @@ describe('trace', () => { }, span => { expect(span).toBeDefined(); - expect(span?.attributes).toEqual({ - [OTEL_ATTR_SOURCE]: 'task', - [OTEL_ATTR_ORIGIN]: 'auto.test.origin', - [OTEL_ATTR_OP]: 'my-op', - [OTEL_ATTR_SENTRY_SAMPLE_RATE]: 1, + expect(getSpanAttributes(span)).toEqual({ + [InternalSentrySemanticAttributes.SOURCE]: 'task', + [InternalSentrySemanticAttributes.ORIGIN]: 'auto.test.origin', + [InternalSentrySemanticAttributes.OP]: 'my-op', + [InternalSentrySemanticAttributes.SAMPLE_RATE]: 1, }); - expect(getSpanMetadata(span!)).toEqual({ requestPath: 'test-path' }); + expect(getSpanMetadata(span)).toEqual({ requestPath: 'test-path' }); }, ); }); @@ -183,51 +235,51 @@ describe('trace', () => { describe('startInactiveSpan', () => { it('works at the root', () => { - const span = Sentry.startInactiveSpan({ name: 'test' }); + const span = startInactiveSpan({ name: 'test' }); expect(span).toBeDefined(); - expect(span?.name).toEqual('test'); - expect(span?.endTime).toEqual([0, 0]); + expect(getSpanName(span)).toEqual('test'); + expect(getSpanEndTime(span)).toEqual([0, 0]); expect(getActiveSpan()).toBeUndefined(); - span?.end(); + span.end(); - expect(span?.endTime).not.toEqual([0, 0]); + expect(getSpanEndTime(span)).not.toEqual([0, 0]); expect(getActiveSpan()).toBeUndefined(); }); it('works as a child span', () => { - Sentry.startSpan({ name: 'outer' }, outerSpan => { + startSpan({ name: 'outer' }, outerSpan => { expect(outerSpan).toBeDefined(); expect(getActiveSpan()).toEqual(outerSpan); - const innerSpan = Sentry.startInactiveSpan({ name: 'test' }); + const innerSpan = startInactiveSpan({ name: 'test' }); expect(innerSpan).toBeDefined(); - expect(innerSpan?.name).toEqual('test'); - expect(innerSpan?.endTime).toEqual([0, 0]); + expect(getSpanName(innerSpan)).toEqual('test'); + expect(getSpanEndTime(innerSpan)).toEqual([0, 0]); expect(getActiveSpan()).toEqual(outerSpan); - innerSpan?.end(); + innerSpan.end(); - expect(innerSpan?.endTime).not.toEqual([0, 0]); + expect(getSpanEndTime(innerSpan)).not.toEqual([0, 0]); expect(getActiveSpan()).toEqual(outerSpan); }); }); it('allows to pass context arguments', () => { - const span = Sentry.startInactiveSpan({ + const span = startInactiveSpan({ name: 'outer', }); expect(span).toBeDefined(); - expect(span?.attributes).toEqual({ - [OTEL_ATTR_SENTRY_SAMPLE_RATE]: 1, + expect(getSpanAttributes(span)).toEqual({ + [InternalSentrySemanticAttributes.SAMPLE_RATE]: 1, }); - expect(getSpanMetadata(span!)).toEqual(undefined); + expect(getSpanMetadata(span)).toEqual(undefined); - const span2 = Sentry.startInactiveSpan({ + const span2 = startInactiveSpan({ name: 'outer', op: 'my-op', origin: 'auto.test.origin', @@ -236,14 +288,14 @@ describe('trace', () => { }); expect(span2).toBeDefined(); - expect(span2?.attributes).toEqual({ - [OTEL_ATTR_SENTRY_SAMPLE_RATE]: 1, - [OTEL_ATTR_SOURCE]: 'task', - [OTEL_ATTR_ORIGIN]: 'auto.test.origin', - [OTEL_ATTR_OP]: 'my-op', + expect(getSpanAttributes(span2)).toEqual({ + [InternalSentrySemanticAttributes.SAMPLE_RATE]: 1, + [InternalSentrySemanticAttributes.SOURCE]: 'task', + [InternalSentrySemanticAttributes.ORIGIN]: 'auto.test.origin', + [InternalSentrySemanticAttributes.OP]: 'my-op', }); - expect(getSpanMetadata(span2!)).toEqual({ requestPath: 'test-path' }); + expect(getSpanMetadata(span2)).toEqual({ requestPath: 'test-path' }); }); }); }); @@ -258,8 +310,9 @@ describe('trace (tracing disabled)', () => { }); it('startSpan calls callback without span', () => { - const val = Sentry.startSpan({ name: 'outer' }, outerSpan => { - expect(outerSpan).toBeUndefined(); + const val = startSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan).toBeDefined(); + expect(outerSpan.isRecording()).toBe(false); return 'test value'; }); @@ -267,10 +320,11 @@ describe('trace (tracing disabled)', () => { expect(val).toEqual('test value'); }); - it('startInactiveSpan returns undefined', () => { - const span = Sentry.startInactiveSpan({ name: 'test' }); + it('startInactiveSpan returns a NonRecordinSpan', () => { + const span = startInactiveSpan({ name: 'test' }); - expect(span).toBeUndefined(); + expect(span).toBeDefined(); + expect(span.isRecording()).toBe(false); }); }); @@ -285,11 +339,13 @@ describe('trace (sampling)', () => { mockSdkInit({ tracesSampleRate: 0.5 }); - Sentry.startSpan({ name: 'outer' }, outerSpan => { - expect(outerSpan).toBeUndefined(); + startSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan).toBeDefined(); + expect(outerSpan.isRecording()).toBe(false); - Sentry.startSpan({ name: 'inner' }, innerSpan => { - expect(innerSpan).toBeUndefined(); + startSpan({ name: 'inner' }, innerSpan => { + expect(innerSpan).toBeDefined(); + expect(innerSpan.isRecording()).toBe(false); }); }); }); @@ -299,16 +355,16 @@ describe('trace (sampling)', () => { mockSdkInit({ tracesSampleRate: 0.5 }); - Sentry.startSpan({ name: 'outer' }, outerSpan => { + startSpan({ name: 'outer' }, outerSpan => { expect(outerSpan).toBeDefined(); - expect(outerSpan?.isRecording()).toBe(true); + expect(outerSpan.isRecording()).toBe(true); // All fields are empty for NonRecordingSpan - expect(outerSpan?.name).toBe('outer'); + expect(getSpanName(outerSpan)).toBe('outer'); - Sentry.startSpan({ name: 'inner' }, innerSpan => { + startSpan({ name: 'inner' }, innerSpan => { expect(innerSpan).toBeDefined(); - expect(innerSpan?.isRecording()).toBe(true); - expect(innerSpan?.name).toBe('inner'); + expect(innerSpan.isRecording()).toBe(true); + expect(getSpanName(innerSpan)).toBe('inner'); }); }); }); @@ -319,20 +375,20 @@ describe('trace (sampling)', () => { mockSdkInit({ tracesSampleRate: 1 }); // This will def. be sampled because of the tracesSampleRate - Sentry.startSpan({ name: 'outer' }, outerSpan => { + startSpan({ name: 'outer' }, outerSpan => { expect(outerSpan).toBeDefined(); - expect(outerSpan?.isRecording()).toBe(true); - expect(outerSpan?.name).toBe('outer'); + expect(outerSpan.isRecording()).toBe(true); + expect(getSpanName(outerSpan)).toBe('outer'); // Now let's mutate the tracesSampleRate so that the next entry _should_ not be sampled // but it will because of parent sampling - const client = Sentry.getCurrentHub().getClient(); + const client = getCurrentHub().getClient(); client!.getOptions().tracesSampleRate = 0.5; - Sentry.startSpan({ name: 'inner' }, innerSpan => { + startSpan({ name: 'inner' }, innerSpan => { expect(innerSpan).toBeDefined(); - expect(innerSpan?.isRecording()).toBe(true); - expect(innerSpan?.name).toBe('inner'); + expect(innerSpan.isRecording()).toBe(true); + expect(getSpanName(innerSpan)).toBe('inner'); }); }); }); @@ -342,17 +398,19 @@ describe('trace (sampling)', () => { mockSdkInit({ tracesSampleRate: 0.5 }); - // This will def. be sampled because of the tracesSampleRate - Sentry.startSpan({ name: 'outer' }, outerSpan => { - expect(outerSpan).toBeUndefined(); + // This will def. be unsampled because of the tracesSampleRate + startSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan).toBeDefined(); + expect(outerSpan.isRecording()).toBe(false); - // Now let's mutate the tracesSampleRate so that the next entry _should_ not be sampled - // but it will because of parent sampling - const client = Sentry.getCurrentHub().getClient(); + // Now let's mutate the tracesSampleRate so that the next entry _should_ be sampled + // but it will remain unsampled because of parent sampling + const client = getCurrentHub().getClient(); client!.getOptions().tracesSampleRate = 1; - Sentry.startSpan({ name: 'inner' }, innerSpan => { - expect(innerSpan).toBeUndefined(); + startSpan({ name: 'inner' }, innerSpan => { + expect(innerSpan).toBeDefined(); + expect(innerSpan.isRecording()).toBe(false); }); }); }); @@ -382,16 +440,13 @@ describe('trace (sampling)', () => { // We simulate the correct context we'd normally get from the SentryPropagator context.with( - trace.setSpanContext( - context.active().setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext), - spanContext, - ), + trace.setSpanContext(setPropagationContextOnContext(context.active(), propagationContext), spanContext), () => { // This will def. be sampled because of the tracesSampleRate - Sentry.startSpan({ name: 'outer' }, outerSpan => { + startSpan({ name: 'outer' }, outerSpan => { expect(outerSpan).toBeDefined(); - expect(outerSpan?.isRecording()).toBe(true); - expect(outerSpan?.name).toBe('outer'); + expect(outerSpan.isRecording()).toBe(true); + expect(getSpanName(outerSpan)).toBe('outer'); }); }, ); @@ -422,14 +477,12 @@ describe('trace (sampling)', () => { // We simulate the correct context we'd normally get from the SentryPropagator context.with( - trace.setSpanContext( - context.active().setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext), - spanContext, - ), + trace.setSpanContext(setPropagationContextOnContext(context.active(), propagationContext), spanContext), () => { // This will def. be sampled because of the tracesSampleRate - Sentry.startSpan({ name: 'outer' }, outerSpan => { - expect(outerSpan).toBeUndefined(); + startSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan).toBeDefined(); + expect(outerSpan.isRecording()).toBe(false); }); }, ); @@ -444,7 +497,7 @@ describe('trace (sampling)', () => { mockSdkInit({ tracesSampler }); - Sentry.startSpan({ name: 'outer' }, outerSpan => { + startSpan({ name: 'outer' }, outerSpan => { expect(outerSpan).toBeDefined(); }); @@ -457,11 +510,11 @@ describe('trace (sampling)', () => { // Now return `false`, it should not sample tracesSamplerResponse = false; - Sentry.startSpan({ name: 'outer2' }, outerSpan => { - expect(outerSpan).toBeUndefined(); + startSpan({ name: 'outer2' }, outerSpan => { + expect(outerSpan.isRecording()).toBe(false); - Sentry.startSpan({ name: 'inner2' }, outerSpan => { - expect(outerSpan).toBeUndefined(); + startSpan({ name: 'inner2' }, innerSpan => { + expect(innerSpan.isRecording()).toBe(false); }); }); @@ -483,7 +536,7 @@ describe('trace (sampling)', () => { mockSdkInit({ tracesSampler }); - Sentry.startSpan({ name: 'outer' }, outerSpan => { + startSpan({ name: 'outer' }, outerSpan => { expect(outerSpan).toBeDefined(); }); @@ -496,11 +549,11 @@ describe('trace (sampling)', () => { // Now return `0`, it should not sample tracesSamplerResponse = 0; - Sentry.startSpan({ name: 'outer2' }, outerSpan => { - expect(outerSpan).toBeUndefined(); + startSpan({ name: 'outer2' }, outerSpan => { + expect(outerSpan.isRecording()).toBe(false); - Sentry.startSpan({ name: 'inner2' }, outerSpan => { - expect(outerSpan).toBeUndefined(); + startSpan({ name: 'inner2' }, innerSpan => { + expect(innerSpan.isRecording()).toBe(false); }); }); @@ -513,8 +566,8 @@ describe('trace (sampling)', () => { // Now return `0.4`, it should not sample tracesSamplerResponse = 0.4; - Sentry.startSpan({ name: 'outer3' }, outerSpan => { - expect(outerSpan).toBeUndefined(); + startSpan({ name: 'outer3' }, outerSpan => { + expect(outerSpan.isRecording()).toBe(false); }); expect(tracesSampler).toHaveBeenCalledTimes(4); @@ -550,14 +603,11 @@ describe('trace (sampling)', () => { // We simulate the correct context we'd normally get from the SentryPropagator context.with( - trace.setSpanContext( - context.active().setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext), - spanContext, - ), + trace.setSpanContext(setPropagationContextOnContext(context.active(), propagationContext), spanContext), () => { // This will def. be sampled because of the tracesSampleRate - Sentry.startSpan({ name: 'outer' }, outerSpan => { - expect(outerSpan).toBeUndefined(); + startSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan.isRecording()).toBe(false); }); }, ); @@ -572,3 +622,15 @@ describe('trace (sampling)', () => { }); }); }); + +function getSpanName(span: AbstractSpan): string | undefined { + return spanHasName(span) ? span.name : undefined; +} + +function getSpanEndTime(span: AbstractSpan): [number, number] | undefined { + return (span as ReadableSpan).endTime; +} + +function getSpanAttributes(span: AbstractSpan): Record | undefined { + return spanHasAttributes(span) ? span.attributes : undefined; +} diff --git a/packages/opentelemetry/test/utils/captureExceptionForTimedEvent.test.ts b/packages/opentelemetry/test/utils/captureExceptionForTimedEvent.test.ts new file mode 100644 index 000000000000..4d0c39b3a8b9 --- /dev/null +++ b/packages/opentelemetry/test/utils/captureExceptionForTimedEvent.test.ts @@ -0,0 +1,147 @@ +import type { Span as OtelSpan, TimedEvent } from '@opentelemetry/sdk-trace-base'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import type { Hub } from '@sentry/types'; + +import { maybeCaptureExceptionForTimedEvent } from '../../src/utils/captureExceptionForTimedEvent'; + +describe('maybeCaptureExceptionForTimedEvent', () => { + it('ignores non-exception events', async () => { + const event: TimedEvent = { + time: [12345, 0], + name: 'test event', + }; + + const captureException = jest.fn(); + const hub = { + captureException, + } as unknown as Hub; + + maybeCaptureExceptionForTimedEvent(hub, event); + + expect(captureException).not.toHaveBeenCalled(); + }); + + it('ignores exception events without EXCEPTION_MESSAGE', async () => { + const event: TimedEvent = { + time: [12345, 0], + name: 'exception', + }; + + const captureException = jest.fn(); + const hub = { + captureException, + } as unknown as Hub; + + maybeCaptureExceptionForTimedEvent(hub, event); + + expect(captureException).not.toHaveBeenCalled(); + }); + + it('captures exception from event with EXCEPTION_MESSAGE', async () => { + const event: TimedEvent = { + time: [12345, 0], + name: 'exception', + attributes: { + [SemanticAttributes.EXCEPTION_MESSAGE]: 'test-message', + }, + }; + + const captureException = jest.fn(); + const hub = { + captureException, + } as unknown as Hub; + + maybeCaptureExceptionForTimedEvent(hub, event); + + expect(captureException).toHaveBeenCalledTimes(1); + expect(captureException).toHaveBeenCalledWith(expect.objectContaining({ message: 'test-message' }), { + captureContext: undefined, + }); + expect(captureException).toHaveBeenCalledWith(expect.any(Error), { + captureContext: undefined, + }); + }); + + it('captures stack and type, if available', async () => { + const event: TimedEvent = { + time: [12345, 0], + name: 'exception', + attributes: { + [SemanticAttributes.EXCEPTION_MESSAGE]: 'test-message', + [SemanticAttributes.EXCEPTION_STACKTRACE]: 'test-stack', + [SemanticAttributes.EXCEPTION_TYPE]: 'test-type', + }, + }; + + const captureException = jest.fn(); + const hub = { + captureException, + } as unknown as Hub; + + maybeCaptureExceptionForTimedEvent(hub, event); + + expect(captureException).toHaveBeenCalledTimes(1); + expect(captureException).toHaveBeenCalledWith( + expect.objectContaining({ message: 'test-message', name: 'test-type', stack: 'test-stack' }), + { + captureContext: undefined, + }, + ); + expect(captureException).toHaveBeenCalledWith(expect.any(Error), { + captureContext: undefined, + }); + }); + + it('captures span context, if available', async () => { + const event: TimedEvent = { + time: [12345, 0], + name: 'exception', + attributes: { + [SemanticAttributes.EXCEPTION_MESSAGE]: 'test-message', + }, + }; + + const span = { + parentSpanId: 'test-parent-span-id', + attributes: { + 'test-attr1': 'test-value1', + }, + resource: { + attributes: { + 'test-attr2': 'test-value2', + }, + }, + spanContext: () => { + return { spanId: 'test-span-id', traceId: 'test-trace-id' }; + }, + } as unknown as OtelSpan; + + const captureException = jest.fn(); + const hub = { + captureException, + } as unknown as Hub; + + maybeCaptureExceptionForTimedEvent(hub, event, span); + + expect(captureException).toHaveBeenCalledTimes(1); + expect(captureException).toHaveBeenCalledWith(expect.objectContaining({ message: 'test-message' }), { + captureContext: { + contexts: { + otel: { + attributes: { + 'test-attr1': 'test-value1', + }, + resource: { + 'test-attr2': 'test-value2', + }, + }, + trace: { + trace_id: 'test-trace-id', + span_id: 'test-span-id', + parent_span_id: 'test-parent-span-id', + }, + }, + }, + }); + }); +}); diff --git a/packages/node-experimental/test/utils/convertOtelTimeToSeconds.test.ts b/packages/opentelemetry/test/utils/convertOtelTimeToSeconds.test.ts similarity index 100% rename from packages/node-experimental/test/utils/convertOtelTimeToSeconds.test.ts rename to packages/opentelemetry/test/utils/convertOtelTimeToSeconds.test.ts diff --git a/packages/node-experimental/test/utils/getActiveSpan.test.ts b/packages/opentelemetry/test/utils/getActiveSpan.test.ts similarity index 88% rename from packages/node-experimental/test/utils/getActiveSpan.test.ts rename to packages/opentelemetry/test/utils/getActiveSpan.test.ts index b97ced5bdbf8..b3a2f359bfbd 100644 --- a/packages/node-experimental/test/utils/getActiveSpan.test.ts +++ b/packages/opentelemetry/test/utils/getActiveSpan.test.ts @@ -1,18 +1,16 @@ import { trace } from '@opentelemetry/api'; import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; -import { NodeExperimentalClient } from '../../src/sdk/client'; -import { setupOtel } from '../../src/sdk/initOtel'; import { getActiveSpan, getRootSpan } from '../../src/utils/getActiveSpan'; -import { getDefaultNodeExperimentalClientOptions } from '../helpers/getDefaultNodePreviewClientOptions'; +import { setupOtel } from '../helpers/initOtel'; import { cleanupOtel } from '../helpers/mockSdkInit'; +import { getDefaultTestClientOptions, TestClient } from '../helpers/TestClient'; describe('getActiveSpan', () => { let provider: BasicTracerProvider | undefined; beforeEach(() => { - const options = getDefaultNodeExperimentalClientOptions(); - const client = new NodeExperimentalClient(options); + const client = new TestClient(getDefaultTestClientOptions()); provider = setupOtel(client); }); @@ -97,8 +95,7 @@ describe('getRootSpan', () => { let provider: BasicTracerProvider | undefined; beforeEach(() => { - const options = getDefaultNodeExperimentalClientOptions(); - const client = new NodeExperimentalClient(options); + const client = new TestClient(getDefaultTestClientOptions()); provider = setupOtel(client); }); diff --git a/packages/node-experimental/test/utils/getRequestSpanData.test.ts b/packages/opentelemetry/test/utils/getRequestSpanData.test.ts similarity index 100% rename from packages/node-experimental/test/utils/getRequestSpanData.test.ts rename to packages/opentelemetry/test/utils/getRequestSpanData.test.ts diff --git a/packages/opentelemetry/test/utils/getSpanKind.test.ts b/packages/opentelemetry/test/utils/getSpanKind.test.ts new file mode 100644 index 000000000000..50e57ee4fac7 --- /dev/null +++ b/packages/opentelemetry/test/utils/getSpanKind.test.ts @@ -0,0 +1,11 @@ +import type { Span } from '@opentelemetry/api'; +import { SpanKind } from '@opentelemetry/api'; + +import { getSpanKind } from '../../src/utils/getSpanKind'; + +describe('getSpanKind', () => { + it('works', () => { + expect(getSpanKind({} as Span)).toBe(SpanKind.INTERNAL); + expect(getSpanKind({ kind: SpanKind.CLIENT } as unknown as Span)).toBe(SpanKind.CLIENT); + }); +}); diff --git a/packages/node-experimental/test/utils/groupSpansWithParents.test.ts b/packages/opentelemetry/test/utils/groupSpansWithParents.test.ts similarity index 100% rename from packages/node-experimental/test/utils/groupSpansWithParents.test.ts rename to packages/opentelemetry/test/utils/groupSpansWithParents.test.ts diff --git a/packages/opentelemetry/test/utils/mapStatus.test.ts b/packages/opentelemetry/test/utils/mapStatus.test.ts new file mode 100644 index 000000000000..4fa6cc664b61 --- /dev/null +++ b/packages/opentelemetry/test/utils/mapStatus.test.ts @@ -0,0 +1,83 @@ +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import type { SpanStatusType } from '@sentry/core'; + +import { mapStatus } from '../../src/utils/mapStatus'; +import { createSpan } from '../helpers/createSpan'; + +describe('mapStatus', () => { + const statusTestTable: [number, undefined | number | string, undefined | string, SpanStatusType][] = [ + [-1, undefined, undefined, 'unknown_error'], + [3, undefined, undefined, 'unknown_error'], + [0, undefined, undefined, 'ok'], + [1, undefined, undefined, 'ok'], + [2, undefined, undefined, 'unknown_error'], + + // http codes + [2, 400, undefined, 'failed_precondition'], + [2, 401, undefined, 'unauthenticated'], + [2, 403, undefined, 'permission_denied'], + [2, 404, undefined, 'not_found'], + [2, 409, undefined, 'aborted'], + [2, 429, undefined, 'resource_exhausted'], + [2, 499, undefined, 'cancelled'], + [2, 500, undefined, 'internal_error'], + [2, 501, undefined, 'unimplemented'], + [2, 503, undefined, 'unavailable'], + [2, 504, undefined, 'deadline_exceeded'], + [2, 999, undefined, 'unknown_error'], + + [2, '400', undefined, 'failed_precondition'], + [2, '401', undefined, 'unauthenticated'], + [2, '403', undefined, 'permission_denied'], + [2, '404', undefined, 'not_found'], + [2, '409', undefined, 'aborted'], + [2, '429', undefined, 'resource_exhausted'], + [2, '499', undefined, 'cancelled'], + [2, '500', undefined, 'internal_error'], + [2, '501', undefined, 'unimplemented'], + [2, '503', undefined, 'unavailable'], + [2, '504', undefined, 'deadline_exceeded'], + [2, '999', undefined, 'unknown_error'], + + // grpc codes + [2, undefined, '1', 'cancelled'], + [2, undefined, '2', 'unknown_error'], + [2, undefined, '3', 'invalid_argument'], + [2, undefined, '4', 'deadline_exceeded'], + [2, undefined, '5', 'not_found'], + [2, undefined, '6', 'already_exists'], + [2, undefined, '7', 'permission_denied'], + [2, undefined, '8', 'resource_exhausted'], + [2, undefined, '9', 'failed_precondition'], + [2, undefined, '10', 'aborted'], + [2, undefined, '11', 'out_of_range'], + [2, undefined, '12', 'unimplemented'], + [2, undefined, '13', 'internal_error'], + [2, undefined, '14', 'unavailable'], + [2, undefined, '15', 'data_loss'], + [2, undefined, '16', 'unauthenticated'], + [2, undefined, '999', 'unknown_error'], + + // http takes precedence over grpc + [2, '400', '2', 'failed_precondition'], + ]; + + it.each(statusTestTable)( + 'works with otelStatus=%i, httpCode=%s, grpcCode=%s', + (otelStatus, httpCode, grpcCode, expected) => { + const span = createSpan(); + span.setStatus({ code: otelStatus }); + + if (httpCode) { + span.setAttribute(SemanticAttributes.HTTP_STATUS_CODE, httpCode); + } + + if (grpcCode) { + span.setAttribute(SemanticAttributes.RPC_GRPC_STATUS_CODE, grpcCode); + } + + const actual = mapStatus(span); + expect(actual).toEqual(expected); + }, + ); +}); diff --git a/packages/opentelemetry/test/utils/parseSpanDescription.test.ts b/packages/opentelemetry/test/utils/parseSpanDescription.test.ts new file mode 100644 index 000000000000..aa78526f8ffe --- /dev/null +++ b/packages/opentelemetry/test/utils/parseSpanDescription.test.ts @@ -0,0 +1,340 @@ +import type { Span } from '@opentelemetry/api'; +import { SpanKind } from '@opentelemetry/api'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; + +import { descriptionForHttpMethod, getSanitizedUrl, parseSpanDescription } from '../../src/utils/parseSpanDescription'; + +describe('parseSpanDescription', () => { + it.each([ + [ + 'works without attributes & name', + undefined, + undefined, + undefined, + { + description: '', + op: undefined, + source: 'custom', + }, + ], + [ + 'works with empty attributes', + {}, + 'test name', + SpanKind.CLIENT, + { + description: 'test name', + op: undefined, + source: 'custom', + }, + ], + [ + 'works with http method', + { + [SemanticAttributes.HTTP_METHOD]: 'GET', + }, + 'test name', + SpanKind.CLIENT, + { + description: 'test name', + op: 'http.client', + source: 'custom', + }, + ], + [ + 'works with db system', + { + [SemanticAttributes.DB_SYSTEM]: 'mysql', + [SemanticAttributes.DB_STATEMENT]: 'SELECT * from users', + }, + 'test name', + SpanKind.CLIENT, + { + description: 'SELECT * from users', + op: 'db', + source: 'task', + }, + ], + [ + 'works with db system without statement', + { + [SemanticAttributes.DB_SYSTEM]: 'mysql', + }, + 'test name', + SpanKind.CLIENT, + { + description: 'test name', + op: 'db', + source: 'task', + }, + ], + [ + 'works with rpc service', + { + [SemanticAttributes.RPC_SERVICE]: 'rpc-test-service', + }, + 'test name', + undefined, + { + description: 'test name', + op: 'rpc', + source: 'route', + }, + ], + [ + 'works with messaging system', + { + [SemanticAttributes.MESSAGING_SYSTEM]: 'test-messaging-system', + }, + 'test name', + undefined, + { + description: 'test name', + op: 'message', + source: 'route', + }, + ], + [ + 'works with faas trigger', + { + [SemanticAttributes.FAAS_TRIGGER]: 'test-faas-trigger', + }, + 'test name', + undefined, + { + description: 'test name', + op: 'test-faas-trigger', + source: 'route', + }, + ], + ])('%s', (_, attributes, name, kind, expected) => { + const actual = parseSpanDescription({ attributes, kind, name } as unknown as Span); + expect(actual).toEqual(expected); + }); +}); + +describe('descriptionForHttpMethod', () => { + it.each([ + [ + 'works withhout attributes', + 'GET', + {}, + 'test name', + SpanKind.CLIENT, + { + op: 'http.client', + description: 'test name', + source: 'custom', + }, + ], + [ + 'works with basic client GET', + 'GET', + { + [SemanticAttributes.HTTP_METHOD]: 'GET', + [SemanticAttributes.HTTP_URL]: 'https://www.example.com/my-path', + [SemanticAttributes.HTTP_TARGET]: '/my-path', + }, + 'test name', + SpanKind.CLIENT, + { + op: 'http.client', + description: 'GET https://www.example.com/my-path', + data: { + url: 'https://www.example.com/my-path', + }, + source: 'url', + }, + ], + [ + 'works with basic server POST', + 'POST', + { + [SemanticAttributes.HTTP_METHOD]: 'POST', + [SemanticAttributes.HTTP_URL]: 'https://www.example.com/my-path', + [SemanticAttributes.HTTP_TARGET]: '/my-path', + }, + 'test name', + SpanKind.SERVER, + { + op: 'http.server', + description: 'POST /my-path', + data: { + url: 'https://www.example.com/my-path', + }, + source: 'url', + }, + ], + [ + 'works with client GET with route', + 'GET', + { + [SemanticAttributes.HTTP_METHOD]: 'GET', + [SemanticAttributes.HTTP_URL]: 'https://www.example.com/my-path/123', + [SemanticAttributes.HTTP_TARGET]: '/my-path/123', + [SemanticAttributes.HTTP_ROUTE]: '/my-path/:id', + }, + 'test name', + SpanKind.CLIENT, + { + op: 'http.client', + description: 'GET /my-path/:id', + data: { + url: 'https://www.example.com/my-path/123', + }, + source: 'route', + }, + ], + ])('%s', (_, httpMethod, attributes, name, kind, expected) => { + const actual = descriptionForHttpMethod({ attributes, kind, name }, httpMethod); + expect(actual).toEqual(expected); + }); +}); + +describe('getSanitizedUrl', () => { + it.each([ + [ + 'works without attributes', + {}, + SpanKind.CLIENT, + { + urlPath: undefined, + url: undefined, + fragment: undefined, + query: undefined, + hasRoute: false, + }, + ], + [ + 'uses url without query for client request', + { + [SemanticAttributes.HTTP_URL]: 'http://example.com/?what=true', + [SemanticAttributes.HTTP_METHOD]: 'GET', + [SemanticAttributes.HTTP_TARGET]: '/?what=true', + [SemanticAttributes.HTTP_HOST]: 'example.com:80', + [SemanticAttributes.HTTP_STATUS_CODE]: 200, + }, + SpanKind.CLIENT, + { + urlPath: 'http://example.com/', + url: 'http://example.com/', + fragment: undefined, + query: '?what=true', + hasRoute: false, + }, + ], + [ + 'uses url without hash for client request', + { + [SemanticAttributes.HTTP_URL]: 'http://example.com/sub#hash', + [SemanticAttributes.HTTP_METHOD]: 'GET', + [SemanticAttributes.HTTP_TARGET]: '/sub#hash', + [SemanticAttributes.HTTP_HOST]: 'example.com:80', + [SemanticAttributes.HTTP_STATUS_CODE]: 200, + }, + SpanKind.CLIENT, + { + urlPath: 'http://example.com/sub', + url: 'http://example.com/sub', + fragment: '#hash', + query: undefined, + hasRoute: false, + }, + ], + [ + 'uses route if available for client request', + { + [SemanticAttributes.HTTP_URL]: 'http://example.com/?what=true', + [SemanticAttributes.HTTP_METHOD]: 'GET', + [SemanticAttributes.HTTP_TARGET]: '/?what=true', + [SemanticAttributes.HTTP_ROUTE]: '/my-route', + [SemanticAttributes.HTTP_HOST]: 'example.com:80', + [SemanticAttributes.HTTP_STATUS_CODE]: 200, + }, + SpanKind.CLIENT, + { + urlPath: '/my-route', + url: 'http://example.com/', + fragment: undefined, + query: '?what=true', + hasRoute: true, + }, + ], + [ + 'falls back to target for client request if url not available', + { + [SemanticAttributes.HTTP_METHOD]: 'GET', + [SemanticAttributes.HTTP_TARGET]: '/?what=true', + [SemanticAttributes.HTTP_HOST]: 'example.com:80', + [SemanticAttributes.HTTP_STATUS_CODE]: 200, + }, + SpanKind.CLIENT, + { + urlPath: '/', + url: undefined, + fragment: undefined, + query: undefined, + hasRoute: false, + }, + ], + [ + 'uses target without query for server request', + { + [SemanticAttributes.HTTP_URL]: 'http://example.com/?what=true', + [SemanticAttributes.HTTP_METHOD]: 'GET', + [SemanticAttributes.HTTP_TARGET]: '/?what=true', + [SemanticAttributes.HTTP_HOST]: 'example.com:80', + [SemanticAttributes.HTTP_STATUS_CODE]: 200, + }, + SpanKind.SERVER, + { + urlPath: '/', + url: 'http://example.com/', + fragment: undefined, + query: '?what=true', + hasRoute: false, + }, + ], + [ + 'uses target without hash for server request', + { + [SemanticAttributes.HTTP_URL]: 'http://example.com/?what=true', + [SemanticAttributes.HTTP_METHOD]: 'GET', + [SemanticAttributes.HTTP_TARGET]: '/sub#hash', + [SemanticAttributes.HTTP_HOST]: 'example.com:80', + [SemanticAttributes.HTTP_STATUS_CODE]: 200, + }, + SpanKind.SERVER, + { + urlPath: '/sub', + url: 'http://example.com/', + fragment: undefined, + query: '?what=true', + hasRoute: false, + }, + ], + [ + 'uses route for server request if available', + { + [SemanticAttributes.HTTP_URL]: 'http://example.com/?what=true', + [SemanticAttributes.HTTP_METHOD]: 'GET', + [SemanticAttributes.HTTP_TARGET]: '/?what=true', + [SemanticAttributes.HTTP_ROUTE]: '/my-route', + [SemanticAttributes.HTTP_HOST]: 'example.com:80', + [SemanticAttributes.HTTP_STATUS_CODE]: 200, + }, + SpanKind.SERVER, + { + urlPath: '/my-route', + url: 'http://example.com/', + fragment: undefined, + query: '?what=true', + hasRoute: true, + }, + ], + ])('%s', (_, attributes, kind, expected) => { + const actual = getSanitizedUrl(attributes, kind); + + expect(actual).toEqual(expected); + }); +}); diff --git a/packages/node-experimental/test/utils/setupEventContextTrace.test.ts b/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts similarity index 77% rename from packages/node-experimental/test/utils/setupEventContextTrace.test.ts rename to packages/opentelemetry/test/utils/setupEventContextTrace.test.ts index 15d7f0976b9e..704225e4eb20 100644 --- a/packages/node-experimental/test/utils/setupEventContextTrace.test.ts +++ b/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts @@ -1,25 +1,24 @@ import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import { makeMain } from '@sentry/core'; -import { NodeExperimentalClient } from '../../src/sdk/client'; -import { NodeExperimentalHub } from '../../src/sdk/hub'; -import { setupOtel } from '../../src/sdk/initOtel'; -import { startSpan } from '../../src/sdk/trace'; -import { setupEventContextTrace } from '../../src/utils/setupEventContextTrace'; -import { getDefaultNodeExperimentalClientOptions } from '../helpers/getDefaultNodePreviewClientOptions'; +import { OpenTelemetryHub } from '../../src/custom/hub'; +import { setupEventContextTrace } from '../../src/setupEventContextTrace'; +import { setupOtel } from '../helpers/initOtel'; import { cleanupOtel } from '../helpers/mockSdkInit'; +import type { TestClientInterface } from '../helpers/TestClient'; +import { getDefaultTestClientOptions, TestClient } from '../helpers/TestClient'; const PUBLIC_DSN = 'https://username@domain/123'; describe('setupEventContextTrace', () => { const beforeSend = jest.fn(() => null); - let client: NodeExperimentalClient; - let hub: NodeExperimentalHub; + let client: TestClientInterface; + let hub: OpenTelemetryHub; let provider: BasicTracerProvider | undefined; beforeEach(() => { - client = new NodeExperimentalClient( - getDefaultNodeExperimentalClientOptions({ + client = new TestClient( + getDefaultTestClientOptions({ sampleRate: 1, enableTracing: true, beforeSend, @@ -28,7 +27,7 @@ describe('setupEventContextTrace', () => { }), ); - hub = new NodeExperimentalHub(client); + hub = new OpenTelemetryHub(client); makeMain(hub); setupEventContextTrace(client); @@ -74,11 +73,11 @@ describe('setupEventContextTrace', () => { let innerId: string | undefined; let traceId: string | undefined; - startSpan({ name: 'outer' }, outerSpan => { + client.tracer.startActiveSpan('outer', outerSpan => { outerId = outerSpan?.spanContext().spanId; traceId = outerSpan?.spanContext().traceId; - startSpan({ name: 'inner' }, innerSpan => { + client.tracer.startActiveSpan('inner', innerSpan => { innerId = innerSpan?.spanContext().spanId; hub.captureException(error); }); diff --git a/packages/node-experimental/test/utils/spanTypes.test.ts b/packages/opentelemetry/test/utils/spanTypes.test.ts similarity index 72% rename from packages/node-experimental/test/utils/spanTypes.test.ts rename to packages/opentelemetry/test/utils/spanTypes.test.ts index fcd4703db9ce..99152204adfa 100644 --- a/packages/node-experimental/test/utils/spanTypes.test.ts +++ b/packages/opentelemetry/test/utils/spanTypes.test.ts @@ -1,13 +1,6 @@ import type { Span } from '@opentelemetry/api'; -import { - spanHasAttributes, - spanHasEvents, - spanHasKind, - spanHasParentId, - spanIsSdkTraceBaseSpan, -} from '../../src/utils/spanTypes'; -import { createSpan } from '../helpers/createSpan'; +import { spanHasAttributes, spanHasEvents, spanHasKind, spanHasParentId } from '../../src/utils/spanTypes'; describe('spanTypes', () => { describe('spanHasAttributes', () => { @@ -77,22 +70,4 @@ describe('spanTypes', () => { } }); }); - - describe('spanIsSdkTraceBaseSpan', () => { - it.each([ - [{}, false], - [createSpan(), true], - ])('works with %p', (span, expected) => { - const castSpan = span as unknown as Span; - const actual = spanIsSdkTraceBaseSpan(castSpan); - - expect(actual).toBe(expected); - - if (actual) { - expect(castSpan.events).toBeDefined(); - expect(castSpan.attributes).toBeDefined(); - expect(castSpan.kind).toBeDefined(); - } - }); - }); }); diff --git a/packages/opentelemetry/tsconfig.json b/packages/opentelemetry/tsconfig.json new file mode 100644 index 000000000000..bf45a09f2d71 --- /dev/null +++ b/packages/opentelemetry/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + + "include": ["src/**/*"], + + "compilerOptions": { + // package-specific options + } +} diff --git a/packages/opentelemetry/tsconfig.test.json b/packages/opentelemetry/tsconfig.test.json new file mode 100644 index 000000000000..87f6afa06b86 --- /dev/null +++ b/packages/opentelemetry/tsconfig.test.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + + "include": ["test/**/*"], + + "compilerOptions": { + // should include all types from `./tsconfig.json` plus types for all test frameworks used + "types": ["node", "jest"] + + // other package-specific, test-specific options + } +} diff --git a/packages/opentelemetry/tsconfig.types.json b/packages/opentelemetry/tsconfig.types.json new file mode 100644 index 000000000000..65455f66bd75 --- /dev/null +++ b/packages/opentelemetry/tsconfig.types.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "build/types" + } +} diff --git a/scripts/node-unit-tests.ts b/scripts/node-unit-tests.ts index fc0f855fd5d4..fb1d90fbb900 100644 --- a/scripts/node-unit-tests.ts +++ b/scripts/node-unit-tests.ts @@ -36,6 +36,7 @@ const SKIP_TEST_PACKAGES: Record = { '@sentry/sveltekit', '@sentry-internal/replay-worker', '@sentry/node-experimental', + '@sentry/opentelemetry', '@sentry/vercel-edge', '@sentry/astro', ], @@ -55,6 +56,7 @@ const SKIP_TEST_PACKAGES: Record = { '@sentry/sveltekit', '@sentry-internal/replay-worker', '@sentry/node-experimental', + '@sentry/opentelemetry', '@sentry/vercel-edge', '@sentry/astro', ], @@ -66,6 +68,7 @@ const SKIP_TEST_PACKAGES: Record = { '@sentry/remix', '@sentry/sveltekit', '@sentry/node-experimental', + '@sentry/opentelemetry', '@sentry/vercel-edge', '@sentry/astro', ], diff --git a/yarn.lock b/yarn.lock index 25c03695adc2..3f33d47b2874 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4275,10 +4275,10 @@ dependencies: "@opentelemetry/context-base" "^0.14.0" -"@opentelemetry/api@1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.4.1.tgz#ff22eb2e5d476fbc2450a196e40dd243cc20c28f" - integrity sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA== +"@opentelemetry/api@1.6.0", "@opentelemetry/api@^1.6.0", "@opentelemetry/api@~1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.6.0.tgz#de2c6823203d6f319511898bb5de7e70f5267e19" + integrity sha512-OWlrQAnWn9577PhVgqjUvMr1pg57Bc4jv0iL4w0PRuOSRvq67rvHW9Ie/dZVMvCzhSCB+UxhcY/PmCmFj33Q+g== "@opentelemetry/api@^0.12.0": version "0.12.0" @@ -4287,15 +4287,10 @@ dependencies: "@opentelemetry/context-base" "^0.12.0" -"@opentelemetry/api@^1.6.0", "@opentelemetry/api@~1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.6.0.tgz#de2c6823203d6f319511898bb5de7e70f5267e19" - integrity sha512-OWlrQAnWn9577PhVgqjUvMr1pg57Bc4jv0iL4w0PRuOSRvq67rvHW9Ie/dZVMvCzhSCB+UxhcY/PmCmFj33Q+g== - -"@opentelemetry/context-async-hooks@1.17.0", "@opentelemetry/context-async-hooks@~1.17.0": - version "1.17.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.17.0.tgz#d198319785bc81533f1a096e86d7f81ce57346ed" - integrity sha512-bDIRCgpKniSyhORU0fTL9ISW6ucU9nruKyXKwYrEBep/2f3uLz8LFyF51ZUK9QxIwBHw6WJudK/2UqttWzER4w== +"@opentelemetry/context-async-hooks@1.17.1", "@opentelemetry/context-async-hooks@^1.17.1", "@opentelemetry/context-async-hooks@~1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.17.1.tgz#4eba80bd66f8cd367e9ba94b5fec5f5acf5d7b25" + integrity sha512-up5I+RiQEkGrVEHtbAtmRgS+ZOnFh3shaDNHqZPBlGy+O92auL6yMmjzYpSKmJOGWowvs3fhVHePa8Exb5iHUg== "@opentelemetry/context-base@^0.12.0": version "0.12.0" @@ -4307,20 +4302,20 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/context-base/-/context-base-0.14.0.tgz#c67fc20a4d891447ca1a855d7d70fa79a3533001" integrity sha512-sDOAZcYwynHFTbLo6n8kIbLiVF3a3BLkrmehJUyEbT9F+Smbi47kLGS2gG2g0fjBLR/Lr1InPD7kXL7FaTqEkw== -"@opentelemetry/core@1.15.2": - version "1.15.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.15.2.tgz#5b170bf223a2333884bbc2d29d95812cdbda7c9f" - integrity sha512-+gBv15ta96WqkHZaPpcDHiaz0utiiHZVfm2YOYSqFGrUaJpPkMoSuLBB58YFQGi6Rsb9EHos84X6X5+9JspmLw== - dependencies: - "@opentelemetry/semantic-conventions" "1.15.2" - -"@opentelemetry/core@1.17.0", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.7.0", "@opentelemetry/core@^1.8.0", "@opentelemetry/core@~1.17.0": +"@opentelemetry/core@1.17.0": version "1.17.0" resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.17.0.tgz#6a72425f5f953dc68b4c7c66d947c018173d30d2" integrity sha512-tfnl3h+UefCgx1aeN2xtrmr6BmdWGKXypk0pflQR0urFS40aE88trnkOMc2HTJZbMrqEEl4HsaBeFhwLVXsrJg== dependencies: "@opentelemetry/semantic-conventions" "1.17.0" +"@opentelemetry/core@1.17.1", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.17.1", "@opentelemetry/core@^1.8.0", "@opentelemetry/core@~1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.17.1.tgz#10c5e09c63aeb1836b34d80baf7113760fb19d96" + integrity sha512-I6LrZvl1FF97FQXPR0iieWQmKnGxYtMbWA1GrAXnLUR+B1Hn2m8KqQNEIlZAucyv00GBgpWkpllmULmZfG8P3g== + dependencies: + "@opentelemetry/semantic-conventions" "1.17.1" + "@opentelemetry/core@^0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-0.12.0.tgz#a888badc9a408fa1f13976a574e69d14be32488e" @@ -4330,17 +4325,17 @@ "@opentelemetry/context-base" "^0.12.0" semver "^7.1.3" -"@opentelemetry/instrumentation-express@~0.33.1": - version "0.33.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-express/-/instrumentation-express-0.33.1.tgz#0710f839d2a395014d2ffef9390074bb60009841" - integrity sha512-awrpiTZWnLOCJ4TeDMTrs6/gH/oXbNipoPx3WUKQlA1yfMlpNynqokTyCYv1n10Zu9Y2P/nIhoNnUw0ywp61nA== +"@opentelemetry/instrumentation-express@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-express/-/instrumentation-express-0.33.2.tgz#e5bd14be5814e24b257cd093220d32d5e9261c5a" + integrity sha512-FR05iNosZL42haYang6vpmcuLfXLngJs/0gAgqXk8vwqGGwilOFak1PjoRdO4PAoso0FI+3zhV3Tz7jyDOmSyA== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.41.2" + "@opentelemetry/instrumentation" "^0.44.0" "@opentelemetry/semantic-conventions" "^1.0.0" - "@types/express" "4.17.17" + "@types/express" "4.17.18" -"@opentelemetry/instrumentation-fastify@~0.32.3": +"@opentelemetry/instrumentation-fastify@0.32.3": version "0.32.3" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.32.3.tgz#2c0640c986018d1a41dfff3d9c3bfe3b5b1cf62d" integrity sha512-vRFVoEJXcu6nNpJ61H5syDb84PirOd4b3u8yl8Bcorrr6firGYBQH4pEIVB4PkQWlmi3sLOifqS3VAO2VRloEQ== @@ -4349,91 +4344,80 @@ "@opentelemetry/instrumentation" "^0.44.0" "@opentelemetry/semantic-conventions" "^1.0.0" -"@opentelemetry/instrumentation-graphql@~0.35.1": - version "0.35.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.35.1.tgz#e49ec2256bcc4820458688abac0212ac781864c0" - integrity sha512-bAM4W5wU0lZ1UIKK/5b4p8LEU8N6W+VgpcnUIK7GTTDxdhcWTd3Q6oyS6nauhZSzEnAEmmJVXaLQAGIU4sEkyA== +"@opentelemetry/instrumentation-graphql@0.35.2": + version "0.35.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.35.2.tgz#67b0c173cff1191cfa66aa26f67c6752c365edf2" + integrity sha512-lJv7BbHFK0ExwogdQMtVHfnWhCBMDQEz8KYvhShXfRPiSStU5aVwa3TmT0O00KiJFpATSKJNZMv1iZNHbF6z1g== dependencies: - "@opentelemetry/instrumentation" "^0.41.2" + "@opentelemetry/instrumentation" "^0.44.0" -"@opentelemetry/instrumentation-http@~0.43.0": - version "0.43.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.43.0.tgz#c21cf8b407e26c912448b110f340ad1eb657a316" - integrity sha512-Ho3IFQFuD0xmcVc0Uq9AvYvROSOuydn4XWRT/h/GO0VCwOeYz/WCwUJvRdS3m1B3AZ4iGJ0q/nhsATp2JX3/gA== +"@opentelemetry/instrumentation-http@0.44.0": + version "0.44.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.44.0.tgz#5a3e4b91073f737f054fe42ef591c39c5b3e6394" + integrity sha512-Nlvj3Y2n9q6uIcQq9f33HbcB4Dr62erSwYA37+vkorYnzI2j9PhxKitocRTZnbYsrymYmQJW9mdq/IAfbtVnNg== dependencies: - "@opentelemetry/core" "1.17.0" - "@opentelemetry/instrumentation" "0.43.0" - "@opentelemetry/semantic-conventions" "1.17.0" + "@opentelemetry/core" "1.17.1" + "@opentelemetry/instrumentation" "0.44.0" + "@opentelemetry/semantic-conventions" "1.17.1" semver "^7.5.2" -"@opentelemetry/instrumentation-mongodb@~0.37.0": - version "0.37.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.37.0.tgz#a7cf4bd9cd5b1053182ec1458c91456812432f55" - integrity sha512-Fwuwf7Fsx/F3QXtU6hbxU4D6DtT33YkAr0+fjtR7qTEcAU0YOxCZfy4tlX2jxjxR1ze8tKfaAWhXBxXwLMWT3g== +"@opentelemetry/instrumentation-mongodb@0.37.1": + version "0.37.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.37.1.tgz#5957565a74a4fe39fb72ab29f3b72a20223ef3df" + integrity sha512-UE+5B/MDfB5MUlJfjj8uo/fMnJPpqeUesJZ/loAWuCLCTDDyEJM7wnAvtH+2c4QoukkkIT1lDe5q9aiXwLEr5g== dependencies: - "@opentelemetry/instrumentation" "^0.41.2" + "@opentelemetry/instrumentation" "^0.44.0" "@opentelemetry/sdk-metrics" "^1.9.1" "@opentelemetry/semantic-conventions" "^1.0.0" -"@opentelemetry/instrumentation-mongoose@~0.33.1": - version "0.33.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.33.1.tgz#0e37eed215fb7fbf8adc0e70199bb8992cb1ea21" - integrity sha512-IzYcEZSmlaOlkyACt8gTl0z3eEQafxzEAt/+W+FdNBiUdm81qpVx/1bpzJwSgIsgcLf27Dl5WsPmrSAi4+Bcng== +"@opentelemetry/instrumentation-mongoose@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.33.2.tgz#99f235df66009e0b73953a58f3f6b9f28e6a31b1" + integrity sha512-JXhhn8vkGKbev6aBPkQ6dL5rDImQfucrub8mU7dknPPpCL850fSQ2qt2qLvyDXfawF5my6KWW0fkKJCeRA+ECw== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.41.2" + "@opentelemetry/instrumentation" "^0.44.0" "@opentelemetry/semantic-conventions" "^1.0.0" -"@opentelemetry/instrumentation-mysql2@~0.34.1": - version "0.34.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.34.1.tgz#d7ce741a7d9a7da270fa791e1c64d8cedd58b5b7" - integrity sha512-SPwgLI2H+gH+GP7b5cWQlFqO/7UeHvw6ZzFKxwLr4vy8wmxYF4aBMLc8qVO8bdXFHd114v0IzOIAvpG6sl/zYQ== +"@opentelemetry/instrumentation-mysql2@0.34.2": + version "0.34.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.34.2.tgz#f59f03c3135a8b50bad9cb3d5b55403008a8d0ba" + integrity sha512-Ac/KAHHtTz087P7I6JapBs+ofNOM+RPTDGwSe1ddnTj0xTAO0F6ITmRC1firnMdzDidI/wI+vmgnWclCB81xKQ== dependencies: - "@opentelemetry/instrumentation" "^0.41.2" + "@opentelemetry/instrumentation" "^0.44.0" "@opentelemetry/semantic-conventions" "^1.0.0" "@opentelemetry/sql-common" "^0.40.0" -"@opentelemetry/instrumentation-mysql@~0.34.1": - version "0.34.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.34.1.tgz#9703d21615dd5ee6b9eda1d74029ba75eec46c9a" - integrity sha512-zQq7hN3ILm1vIJCGeKHRc4pTK8LOmkTt8oKWf0v+whFs7axieIhXZMoCqIBm6BigLy3Trg5iaKyuSrx7kO6q2g== +"@opentelemetry/instrumentation-mysql@0.34.2": + version "0.34.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.34.2.tgz#3372dc11010dce2f357a89a1e3f32359c4d34079" + integrity sha512-3OEhW1CB7b93PHIbQ5t8Aoj/dCqNWQBDBbyUXGy2zFbhEcJBVcLeBpy3w8VEjzNTfRC6cVwASuHRP0aLBIPNjQ== dependencies: - "@opentelemetry/instrumentation" "^0.41.2" + "@opentelemetry/instrumentation" "^0.44.0" "@opentelemetry/semantic-conventions" "^1.0.0" - "@types/mysql" "2.15.21" + "@types/mysql" "2.15.22" -"@opentelemetry/instrumentation-nestjs-core@~0.33.1": - version "0.33.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.33.1.tgz#a6e0175bcda25e455339a5527268e746be969297" - integrity sha512-Y5Khvp8ODA6TuDcZKAc63cYDeeZAA/n0ceF0pcVCJwA2NBeD0hmTrCJXES2cvt7wVbHV/SYCu7OpYDQkNjbBWw== +"@opentelemetry/instrumentation-nestjs-core@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.33.2.tgz#fb87031097a96c761db0823c2eff8deba452abbf" + integrity sha512-jrX/355K+myc5V/EQFouqQzBfy5qj+SyVMHIKqVymOx/zWFCvz1p9ChNiPOKzl2il3o/P/aOqBUN/qnRaGowlw== dependencies: - "@opentelemetry/instrumentation" "^0.41.2" + "@opentelemetry/instrumentation" "^0.44.0" "@opentelemetry/semantic-conventions" "^1.0.0" -"@opentelemetry/instrumentation-pg@~0.36.1": - version "0.36.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.36.1.tgz#66e3aa10948c6e3188d04676dbf304ae8571ce2f" - integrity sha512-k8L7RSRTQ6e+DbHEXZB8Tmf/efkQnWKeClpZb3TEdb34Pvme4PmcpG2zb6JtM99nNrshNlVDLCZ90U3xDneTbw== +"@opentelemetry/instrumentation-pg@0.36.2": + version "0.36.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.36.2.tgz#45947d19bbafabf5b350a76350ef4523deac13a5" + integrity sha512-KUjI8OGi7kicml2Sd/PR/M8otZoZEdPArMfhznS6OQKit+RxFo0p5x6RVeka/cLQlmoc3eeGBizDeZetssbHgw== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.41.2" + "@opentelemetry/instrumentation" "^0.44.0" "@opentelemetry/semantic-conventions" "^1.0.0" "@opentelemetry/sql-common" "^0.40.0" "@types/pg" "8.6.1" - "@types/pg-pool" "2.0.3" + "@types/pg-pool" "2.0.4" -"@opentelemetry/instrumentation@0.41.2", "@opentelemetry/instrumentation@^0.41.2": - version "0.41.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.41.2.tgz#cae11fa64485dcf03dae331f35b315b64bc6189f" - integrity sha512-rxU72E0pKNH6ae2w5+xgVYZLzc5mlxAbGzF4shxMVK8YC2QQsfN38B2GPbj0jvrKWWNUElfclQ+YTykkNg/grw== - dependencies: - "@types/shimmer" "^1.0.2" - import-in-the-middle "1.4.2" - require-in-the-middle "^7.1.1" - semver "^7.5.1" - shimmer "^1.2.1" - -"@opentelemetry/instrumentation@0.43.0", "@opentelemetry/instrumentation@^0.43.0", "@opentelemetry/instrumentation@~0.43.0": +"@opentelemetry/instrumentation@0.43.0", "@opentelemetry/instrumentation@^0.43.0": version "0.43.0" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.43.0.tgz#749521415df03396f969bf42341fcb4acd2e9c7b" integrity sha512-S1uHE+sxaepgp+t8lvIDuRgyjJWisAb733198kwQTUc9ZtYQ2V2gmyCtR1x21ePGVLoMiX/NWY7WA290hwkjJQ== @@ -4444,7 +4428,7 @@ semver "^7.5.2" shimmer "^1.2.1" -"@opentelemetry/instrumentation@^0.44.0": +"@opentelemetry/instrumentation@0.44.0", "@opentelemetry/instrumentation@^0.44.0": version "0.44.0" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.44.0.tgz#194f16fc96671575b6bd73d3fadffb5aa4497e67" integrity sha512-B6OxJTRRCceAhhnPDBshyQO7K07/ltX3quOLu0icEvPK9QZ7r9P1y0RQX8O5DxB4vTv4URRkxkg+aFU/plNtQw== @@ -4455,29 +4439,21 @@ semver "^7.5.2" shimmer "^1.2.1" -"@opentelemetry/propagator-b3@1.17.0": - version "1.17.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-b3/-/propagator-b3-1.17.0.tgz#32509a8214b7ced7709fd06c0ee5a0d86adcc51f" - integrity sha512-oklstXImtaly4vDaL+rGtX41YXZR50jp5a7CSEPMcStp1B7ozdZ5G2I5wftrDvOlOcLt/TIkGWDCr/OkVN7kWg== - dependencies: - "@opentelemetry/core" "1.17.0" - -"@opentelemetry/propagator-jaeger@1.17.0": - version "1.17.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.17.0.tgz#a89dbc34447db0b5029b719baaa111f268b52265" - integrity sha512-iZzu8K0QkZZ16JH9yox6hZk7/Rxc4SPeGU37pvlB9DtzfNxAEX1FMK9zvowv3ve7r2uzZNpa7JGVUwpy5ewdHQ== +"@opentelemetry/propagator-b3@1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-b3/-/propagator-b3-1.17.1.tgz#65dbddf3763db82632ddd7ad1735e597ab7b2dc4" + integrity sha512-XEbXYb81AM3ayJLlbJqITPIgKBQCuby45ZHiB9mchnmQOffh6ZJOmXONdtZAV7TWzmzwvAd28vGSUk57Aw/5ZA== dependencies: - "@opentelemetry/core" "1.17.0" + "@opentelemetry/core" "1.17.1" -"@opentelemetry/resources@1.15.2": - version "1.15.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.15.2.tgz#0c9e26cb65652a1402834a3c030cce6028d6dd9d" - integrity sha512-xmMRLenT9CXmm5HMbzpZ1hWhaUowQf8UB4jMjFlAxx1QzQcsD3KFNAVX/CAWzFPtllTyTplrA4JrQ7sCH3qmYw== +"@opentelemetry/propagator-jaeger@1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.17.1.tgz#31cc43045a059d1ed3651b9f21d0fd6db817b02f" + integrity sha512-p+P4lf2pbqd3YMfZO15QCGsDwR2m1ke2q5+dq6YBLa/q0qiC2eq4cD/qhYBBed5/X4PtdamaVGHGsp+u3GXHDA== dependencies: - "@opentelemetry/core" "1.15.2" - "@opentelemetry/semantic-conventions" "1.15.2" + "@opentelemetry/core" "1.17.1" -"@opentelemetry/resources@1.17.0", "@opentelemetry/resources@~1.17.0": +"@opentelemetry/resources@1.17.0": version "1.17.0" resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.17.0.tgz#ee29144cfd7d194c69698c8153dbadec7fe6819f" integrity sha512-+u0ciVnj8lhuL/qGRBPeVYvk7fL+H/vOddfvmOeJaA1KC+5/3UED1c9KoZQlRsNT5Kw1FaK8LkY2NVLYfOVZQw== @@ -4485,6 +4461,14 @@ "@opentelemetry/core" "1.17.0" "@opentelemetry/semantic-conventions" "1.17.0" +"@opentelemetry/resources@1.17.1", "@opentelemetry/resources@~1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.17.1.tgz#932f70f58c0e03fb1d38f0cba12672fd70804d99" + integrity sha512-M2e5emqg5I7qRKqlzKx0ROkcPyF8PbcSaWEdsm72od9txP7Z/Pl8PDYOyu80xWvbHAWk5mDxOF6v3vNdifzclA== + dependencies: + "@opentelemetry/core" "1.17.1" + "@opentelemetry/semantic-conventions" "1.17.1" + "@opentelemetry/resources@^0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-0.12.0.tgz#5eb287c3032a2bebb2bb9f69b44bd160d2a7d591" @@ -4494,24 +4478,15 @@ "@opentelemetry/core" "^0.12.0" "@opentelemetry/sdk-metrics@^1.9.1": - version "1.17.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics/-/sdk-metrics-1.17.0.tgz#e51d39e0bb749780d17f9b1df12f0490438dec1a" - integrity sha512-HlWM27yGmYuwCoVRe3yg2PqKnIsq0kEF0HQgvkeDWz2NYkq9fFaSspR6kvjxUTbghAlZrabiqbgyKoYpYaXS3w== + version "1.17.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics/-/sdk-metrics-1.17.1.tgz#9c4d13d845bcc82be8684050d9db7cce10f61580" + integrity sha512-eHdpsMCKhKhwznxvEfls8Wv3y4ZBWkkXlD3m7vtHIiWBqsMHspWSfie1s07mM45i/bBCf6YBMgz17FUxIXwmZA== dependencies: - "@opentelemetry/core" "1.17.0" - "@opentelemetry/resources" "1.17.0" + "@opentelemetry/core" "1.17.1" + "@opentelemetry/resources" "1.17.1" lodash.merge "^4.6.2" -"@opentelemetry/sdk-trace-base@1.15.2": - version "1.15.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.15.2.tgz#4821f94033c55a6c8bbd35ae387b715b6108517a" - integrity sha512-BEaxGZbWtvnSPchV98qqqqa96AOcb41pjgvhfzDij10tkBhIu9m0Jd6tZ1tJB5ZHfHbTffqYVYE0AOGobec/EQ== - dependencies: - "@opentelemetry/core" "1.15.2" - "@opentelemetry/resources" "1.15.2" - "@opentelemetry/semantic-conventions" "1.15.2" - -"@opentelemetry/sdk-trace-base@1.17.0", "@opentelemetry/sdk-trace-base@^1.17.0", "@opentelemetry/sdk-trace-base@~1.17.0": +"@opentelemetry/sdk-trace-base@1.17.0": version "1.17.0" resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.17.0.tgz#05a21763c9efa72903c20b8930293cdde344b681" integrity sha512-2T5HA1/1iE36Q9eg6D4zYlC4Y4GcycI1J6NsHPKZY9oWfAxWsoYnRlkPfUqyY5XVtocCo/xHpnJvGNHwzT70oQ== @@ -4520,28 +4495,37 @@ "@opentelemetry/resources" "1.17.0" "@opentelemetry/semantic-conventions" "1.17.0" -"@opentelemetry/sdk-trace-node@^1.17.0": - version "1.17.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.17.0.tgz#c2f23665e5a6878a7ad6a372dac72e7ab05c4eb5" - integrity sha512-Twlaje+t16b5j62CfcaKU869rP9oyBG/sVQWBI5+kDaWuP/YIFnF4LbovaEahK9GwAnW8vPIn6iYLAl/jZBidA== +"@opentelemetry/sdk-trace-base@1.17.1", "@opentelemetry/sdk-trace-base@^1.17.1", "@opentelemetry/sdk-trace-base@~1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.17.1.tgz#8ede213df8b0c957028a869c66964e535193a4fd" + integrity sha512-pfSJJSjZj5jkCJUQZicSpzN8Iz9UKMryPWikZRGObPnJo6cUSoKkjZh6BM3j+D47G4olMBN+YZKYqkFM1L6zNA== dependencies: - "@opentelemetry/context-async-hooks" "1.17.0" - "@opentelemetry/core" "1.17.0" - "@opentelemetry/propagator-b3" "1.17.0" - "@opentelemetry/propagator-jaeger" "1.17.0" - "@opentelemetry/sdk-trace-base" "1.17.0" - semver "^7.5.2" + "@opentelemetry/core" "1.17.1" + "@opentelemetry/resources" "1.17.1" + "@opentelemetry/semantic-conventions" "1.17.1" -"@opentelemetry/semantic-conventions@1.15.2": - version "1.15.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.15.2.tgz#3bafb5de3e20e841dff6cb3c66f4d6e9694c4241" - integrity sha512-CjbOKwk2s+3xPIMcd5UNYQzsf+v94RczbdNix9/kQh38WiQkM90sUOi3if8eyHFgiBjBjhwXrA7W3ydiSQP9mw== +"@opentelemetry/sdk-trace-node@^1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.17.1.tgz#746c197ad54a8e0cdb24a4b257d33dc3a04493c1" + integrity sha512-J56DaG4cusjw5crpI7x9rv4bxDF27DtKYGxXJF56KIvopbNKpdck5ZWXBttEyqgAVPDwHMAXWDL1KchHzF0a3A== + dependencies: + "@opentelemetry/context-async-hooks" "1.17.1" + "@opentelemetry/core" "1.17.1" + "@opentelemetry/propagator-b3" "1.17.1" + "@opentelemetry/propagator-jaeger" "1.17.1" + "@opentelemetry/sdk-trace-base" "1.17.1" + semver "^7.5.2" -"@opentelemetry/semantic-conventions@1.17.0", "@opentelemetry/semantic-conventions@^1.0.0", "@opentelemetry/semantic-conventions@^1.17.0", "@opentelemetry/semantic-conventions@~1.17.0": +"@opentelemetry/semantic-conventions@1.17.0": version "1.17.0" resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.17.0.tgz#af10baa9f05ce1e64a14065fc138b5739bfb65f6" integrity sha512-+fguCd2d8d2qruk0H0DsCEy2CTK3t0Tugg7MhZ/UQMvmewbZLNnJ6heSYyzIZWG5IPfAXzoj4f4F/qpM7l4VBA== +"@opentelemetry/semantic-conventions@1.17.1", "@opentelemetry/semantic-conventions@^1.0.0", "@opentelemetry/semantic-conventions@^1.17.0", "@opentelemetry/semantic-conventions@^1.17.1", "@opentelemetry/semantic-conventions@~1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.17.1.tgz#93d219935e967fbb9aa0592cc96b2c0ec817a56f" + integrity sha512-xbR2U+2YjauIuo42qmE8XyJK6dYeRMLJuOlUP5SO4auET4VtOHOzgkRVOq+Ik18N+Xf3YPcqJs9dZMiDddz1eQ== + "@opentelemetry/semantic-conventions@^0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-0.12.0.tgz#7e392aecdbdbd5d737d3995998b120dc17589ab0" @@ -4605,14 +4589,14 @@ resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz#bf5e2373ca68ce7556b967cb4965a7095e93fe53" integrity sha512-e3k2Vd606efd1ZYy2NQKkT4C/pn31nehyLhVug6To/q8JT8FpiMrDy7zmm3KLF0L98NOQQcutaVtAPhzKhzn9w== -"@prisma/instrumentation@~5.3.1": - version "5.3.1" - resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-5.3.1.tgz#438b8f1b8b14b190dd7bf1bbc903af4c40856648" - integrity sha512-g4epN9WdsyvX24nuoY7ie9uEsLuNUvYA2ShY9D1Ouz0STMltq1iCWAHugKXYKdKFRtoNP8Vo/QtVLQEEqvNQJQ== +"@prisma/instrumentation@5.4.2": + version "5.4.2" + resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-5.4.2.tgz#e1615cb50485f029a47e79378d3edac483d6a5f3" + integrity sha512-VSBfo0VS6aY1fIuMBbeLBaTmmgZxszMn2DvHRnGzEnqD/B9/Yfiu96+c0SKuYr7VkuXlbmt5dpbkJutvuJzZBQ== dependencies: - "@opentelemetry/api" "1.4.1" - "@opentelemetry/instrumentation" "0.41.2" - "@opentelemetry/sdk-trace-base" "1.15.2" + "@opentelemetry/api" "1.6.0" + "@opentelemetry/instrumentation" "0.43.0" + "@opentelemetry/sdk-trace-base" "1.17.0" "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" @@ -5779,10 +5763,10 @@ "@types/qs" "*" "@types/serve-static" "*" -"@types/express@4.17.17": - version "4.17.17" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4" - integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== +"@types/express@4.17.18": + version "4.17.18" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.18.tgz#efabf5c4495c1880df1bdffee604b143b29c4a95" + integrity sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ== dependencies: "@types/body-parser" "*" "@types/express-serve-static-core" "^4.17.33" @@ -5989,7 +5973,14 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.32.tgz#f6cd08939ae3ad886fcc92ef7f0109dacddf61ab" integrity sha512-xPSg0jm4mqgEkNhowKgZFBNtwoEwF6gJ4Dhww+GFpm3IgtNseHQZ5IqdNwnquZEoANxyDAKDRAdVo4Z72VvD/g== -"@types/mysql@2.15.21", "@types/mysql@^2.15.21": +"@types/mysql@2.15.22": + version "2.15.22" + resolved "https://registry.yarnpkg.com/@types/mysql/-/mysql-2.15.22.tgz#8705edb9872bf4aa9dbc004cd494e00334e5cdb4" + integrity sha512-wK1pzsJVVAjYCSZWQoWHziQZbNggXFDUEIGf54g4ZM/ERuP86uGdWeKZWMYlqTPMZfHJJvLPyogXGvCOg87yLQ== + dependencies: + "@types/node" "*" + +"@types/mysql@^2.15.21": version "2.15.21" resolved "https://registry.yarnpkg.com/@types/mysql/-/mysql-2.15.21.tgz#7516cba7f9d077f980100c85fd500c8210bd5e45" integrity sha512-NPotx5CVful7yB+qZbWtXL2fA4e7aEHkihHLjklc6ID8aq7bhguHgeIoC1EmSNTAuCgI6ZXrjt2ZSaXnYX0EUg== @@ -6061,10 +6052,10 @@ resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb" integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g== -"@types/pg-pool@2.0.3": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/pg-pool/-/pg-pool-2.0.3.tgz#3eb8df2933f617f219a53091ad4080c94ba1c959" - integrity sha512-fwK5WtG42Yb5RxAwxm3Cc2dJ39FlgcaNiXKvtTLAwtCn642X7dgel+w1+cLWwpSOFImR3YjsZtbkfjxbHtFAeg== +"@types/pg-pool@2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/pg-pool/-/pg-pool-2.0.4.tgz#b5c60f678094ff3acf3442628a7f708928fcf263" + integrity sha512-qZAvkv1K3QbmHHFYSNRYPkRjOWRLBYrL4B9c+wG0GSVGBw0NtJwPcgx/DSddeDJvRGMHCEQ4VMEVfuJ/0gZ3XQ== dependencies: "@types/pg" "*" @@ -26972,7 +26963,7 @@ semver@7.5.3: dependencies: lru-cache "^6.0.0" -semver@7.5.4, semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.1, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4: +semver@7.5.4, semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== From aea3905597227d7b53e1a8cf811234cd521971b2 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 19 Oct 2023 10:20:15 +0200 Subject: [PATCH 11/38] feat(replay): Allow to treeshake rrweb features (#9274) This depends on https://github.com/getsentry/rrweb/pull/114 to be merged first, but allows to configure build time flags to shake out certain rrweb features that may not be used. It also adds a size limit entry that shows the total bundle size with everything that can be shaken out removed, incl. debug stuff. Bundle size is about ~11kb gzipped less in this scenario, which is not bad. --- .size-limit.js | 19 +++ package.json | 1 + packages/replay/package.json | 4 +- rollup/bundleHelpers.js | 10 +- rollup/npmHelpers.js | 15 +- rollup/plugins/npmPlugins.js | 29 ++++ yarn.lock | 258 +++++++++++++++++++++++++++++++---- 7 files changed, 308 insertions(+), 28 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 4bcda6b45e4a..d375f6a5d760 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -7,6 +7,25 @@ module.exports = [ gzip: true, limit: '90 KB', }, + { + name: '@sentry/browser (incl. Tracing, Replay) - Webpack with treeshaking flags (gzipped)', + path: 'packages/browser/build/npm/esm/index.js', + import: '{ init, Replay, BrowserTracing }', + gzip: true, + limit: '90 KB', + modifyWebpackConfig: function (config) { + const webpack = require('webpack'); + config.plugins.push( + new webpack.DefinePlugin({ + __SENTRY_DEBUG__: false, + __RRWEB_EXCLUDE_CANVAS__: true, + __RRWEB_EXCLUDE_SHADOW_DOM__: true, + __RRWEB_EXCLUDE_IFRAME__: true, + }), + ); + return config; + }, + }, { name: '@sentry/browser (incl. Tracing) - Webpack (gzipped)', path: 'packages/browser/build/npm/esm/index.js', diff --git a/package.json b/package.json index 4217fce71593..59ebf856ed09 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "@rollup/plugin-sucrase": "^4.0.3", "@rollup/plugin-typescript": "^8.3.1", "@size-limit/preset-small-lib": "~9.0.0", + "@size-limit/webpack": "~9.0.0", "@strictsoftware/typedoc-plugin-monorepo": "^0.3.1", "@types/chai": "^4.1.3", "@types/jest": "^27.4.1", diff --git a/packages/replay/package.json b/packages/replay/package.json index c91d06510008..ccbbd1fce9f2 100644 --- a/packages/replay/package.json +++ b/packages/replay/package.json @@ -52,8 +52,8 @@ "devDependencies": { "@babel/core": "^7.17.5", "@sentry-internal/replay-worker": "7.74.1", - "@sentry-internal/rrweb": "2.0.1", - "@sentry-internal/rrweb-snapshot": "2.0.1", + "@sentry-internal/rrweb": "2.1.0", + "@sentry-internal/rrweb-snapshot": "2.1.0", "jsdom-worker": "^0.2.1" }, "dependencies": { diff --git a/rollup/bundleHelpers.js b/rollup/bundleHelpers.js index b2d25b58d248..537cd9321360 100644 --- a/rollup/bundleHelpers.js +++ b/rollup/bundleHelpers.js @@ -10,6 +10,7 @@ import { makeBrowserBuildPlugin, makeCommonJSPlugin, makeIsDebugBuildPlugin, + makeRrwebBuildPlugin, makeLicensePlugin, makeNodeResolvePlugin, makeCleanupPlugin, @@ -34,6 +35,11 @@ export function makeBaseBundleConfig(options) { const markAsBrowserBuildPlugin = makeBrowserBuildPlugin(true); const licensePlugin = makeLicensePlugin(licenseTitle); const tsPlugin = makeTSPlugin('es5'); + const rrwebBuildPlugin = makeRrwebBuildPlugin({ + excludeCanvas: true, + excludeIframe: false, + excludeShadowDom: false, + }); // The `commonjs` plugin is the `esModuleInterop` of the bundling world. When used with `transformMixedEsModules`, it // will include all dependencies, imported or required, in the final bundle. (Without it, CJS modules aren't included @@ -51,7 +57,7 @@ export function makeBaseBundleConfig(options) { }, }, context: 'window', - plugins: [markAsBrowserBuildPlugin], + plugins: [rrwebBuildPlugin, markAsBrowserBuildPlugin], }; // used by `@sentry/integrations` and `@sentry/wasm` (bundles which need to be combined with a stand-alone SDK bundle) @@ -84,7 +90,7 @@ export function makeBaseBundleConfig(options) { // code to add after the CJS wrapper footer: '}(window));', }, - plugins: [markAsBrowserBuildPlugin], + plugins: [rrwebBuildPlugin, markAsBrowserBuildPlugin], }; // used by `@sentry/serverless`, when creating the lambda layer diff --git a/rollup/npmHelpers.js b/rollup/npmHelpers.js index fe2a55543e59..1b99d7911a55 100644 --- a/rollup/npmHelpers.js +++ b/rollup/npmHelpers.js @@ -12,6 +12,7 @@ import { makeNodeResolvePlugin, makeCleanupPlugin, makeSucrasePlugin, + makeRrwebBuildPlugin, makeDebugBuildStatementReplacePlugin, makeSetSDKSourcePlugin, } from './plugins/index.js'; @@ -34,6 +35,11 @@ export function makeBaseNPMConfig(options = {}) { const cleanupPlugin = makeCleanupPlugin(); const extractPolyfillsPlugin = makeExtractPolyfillsPlugin(); const setSdkSourcePlugin = makeSetSDKSourcePlugin('npm'); + const rrwebBuildPlugin = makeRrwebBuildPlugin({ + excludeCanvas: undefined, + excludeShadowDom: undefined, + excludeIframe: undefined, + }); const defaultBaseConfig = { input: entrypoints, @@ -84,7 +90,14 @@ export function makeBaseNPMConfig(options = {}) { interop: esModuleInterop ? 'auto' : 'esModule', }, - plugins: [nodeResolvePlugin, setSdkSourcePlugin, sucrasePlugin, debugBuildStatementReplacePlugin, cleanupPlugin], + plugins: [ + nodeResolvePlugin, + setSdkSourcePlugin, + sucrasePlugin, + debugBuildStatementReplacePlugin, + rrwebBuildPlugin, + cleanupPlugin, + ], // don't include imported modules from outside the package in the final output external: [ diff --git a/rollup/plugins/npmPlugins.js b/rollup/plugins/npmPlugins.js index 5265f5007755..c16d8fd907a3 100644 --- a/rollup/plugins/npmPlugins.js +++ b/rollup/plugins/npmPlugins.js @@ -105,4 +105,33 @@ export function makeDebugBuildStatementReplacePlugin() { }); } +/** + * Creates a plugin to replace build flags of rrweb with either a constant (if passed true/false) or with a safe statement that: + * a) evaluates to `true` + * b) can easily be modified by our users' bundlers to evaluate to false, facilitating the treeshaking of logger code. + * + * When `undefined` is passed, + * end users can define e.g. `__SENTRY_EXCLUDE_CANVAS__` in their bundler to shake out canvas specific rrweb code. + */ +export function makeRrwebBuildPlugin({ excludeCanvas, excludeShadowDom, excludeIframe } = {}) { + const values = {}; + + if (typeof excludeCanvas === 'boolean') { + values['__RRWEB_EXCLUDE_CANVAS__'] = excludeCanvas; + } + + if (typeof excludeShadowDom === 'boolean') { + values['__RRWEB_EXCLUDE_SHADOW_DOM__'] = excludeShadowDom; + } + + if (typeof excludeIframe === 'boolean') { + values['__RRWEB_EXCLUDE_IFRAME__'] = excludeIframe; + } + + return replace({ + preventAssignment: true, + values, + }); +} + export { makeExtractPolyfillsPlugin } from './extractPolyfillsPlugin.js'; diff --git a/yarn.lock b/yarn.lock index 3f33d47b2874..4cc143db4b20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3719,6 +3719,14 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" +"@jridgewell/source-map@^0.3.3": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91" + integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.13": version "1.4.14" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" @@ -4917,33 +4925,33 @@ semver "7.3.2" semver-intersect "1.4.0" -"@sentry-internal/rrdom@2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.0.1.tgz#5d41892ff26462bb5e2412c2f2c646ef2dcfe0b5" - integrity sha512-uPQyq/ANoXSS5HpYkv9qupRSYh/tfbX4xBgM7XZDlApsnD3t6LxAqdAUP//zQO/z+kOHzJVUX5H5uiauqA96Yg== +"@sentry-internal/rrdom@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.1.0.tgz#3e8822cd8f748de5c5a3c58121fac1eebbb767d5" + integrity sha512-99paancC1dkU9O1oUP9zAxfXupX+ha9Cglf9oINUsY/Ey8a6fOhFf5Z3wTzDne28OZ0KeivhBHc8yxeGzdCfGw== dependencies: - "@sentry-internal/rrweb-snapshot" "2.0.1" + "@sentry-internal/rrweb-snapshot" "2.1.0" -"@sentry-internal/rrweb-snapshot@2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.0.1.tgz#5467041c33815d7c07ec0e484a85418d31857ddc" - integrity sha512-C4fIzcpreOzDXkyPOBwGir9YvLiT9jeTa2WQ96U1RVRiLBvXhEyPKgMxWXQcyYTpzYtGwX9dLfHR29uOejzzxQ== +"@sentry-internal/rrweb-snapshot@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.1.0.tgz#7ed2d51bc8f580496676460bcccf37e1f6da5902" + integrity sha512-nqzSIO25We6XIGDwmZ66zRZ7QHGZApck4gbgFYXA/lcCB/zcY0aPD0DTv85oIVUfEo2RCjLeNoWWKwlrrRWtUQ== -"@sentry-internal/rrweb-types@2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.0.1.tgz#4f465715df2959cde486fe77fdda528d85a3c7f7" - integrity sha512-MQRdjsKm/kypHqumsWN+cmFhU0OWWoJSPNxOEG1efbUxZPvZL64tZSrgWimfisIId9TPDn0tr58sBhIgpqgNuw== +"@sentry-internal/rrweb-types@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.1.0.tgz#2888915faec726937db86b4bb9f56e958fdef5e9" + integrity sha512-h9pCw59SJYormxY/R2O/olcynp6xAMzhzZ6lnQy6ezzDfsjjf4G83oqXnAhs6hmqBmDn1QbdODFewMHK6uVxYw== dependencies: - "@sentry-internal/rrweb-snapshot" "2.0.1" + "@sentry-internal/rrweb-snapshot" "2.1.0" -"@sentry-internal/rrweb@2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.0.1.tgz#fa3a60d1e01362ba2ce58583f87bfa076d77ee3b" - integrity sha512-X33eL2CioQn0vOgkFVgu9L8LV4D4H48LFz7cqAofnWC5h6n36zsf7eIBpdDJKZ8JCj1z52h9gL5X+X4W2i/yXQ== +"@sentry-internal/rrweb@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.1.0.tgz#cc8166e9be4cdd2869d81bd65b4ba818063a491f" + integrity sha512-CXBZjl+TtRfPYjLtj5SX/ipqBtZLw5Z3MRIppODi/H7l7oQekOadUHu0+23lm82fl3CXK4jn9gMWcRRulUkn4Q== dependencies: - "@sentry-internal/rrdom" "2.0.1" - "@sentry-internal/rrweb-snapshot" "2.0.1" - "@sentry-internal/rrweb-types" "2.0.1" + "@sentry-internal/rrdom" "2.1.0" + "@sentry-internal/rrweb-snapshot" "2.1.0" + "@sentry-internal/rrweb-types" "2.1.0" "@types/css-font-loading-module" "0.0.7" "@xstate/fsm" "^1.4.0" base64-arraybuffer "^1.0.1" @@ -5205,6 +5213,14 @@ "@size-limit/file" "9.0.0" size-limit "9.0.0" +"@size-limit/webpack@~9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@size-limit/webpack/-/webpack-9.0.0.tgz#4514851d3607490e228bf22bc95286643f64a490" + integrity sha512-0YwdvmBj9rS4bXE/PY9vSdc5lCiQXmT0794EsG7yvlDMWyrWa/dsgcRok/w0MoZstfuLaS6lv03VI5UJRFU/lg== + dependencies: + nanoid "^3.3.6" + webpack "^5.88.2" + "@socket.io/base64-arraybuffer@~1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#568d9beae00b0d835f4f8c53fd55714986492e61" @@ -6711,6 +6727,14 @@ "@webassemblyjs/helper-numbers" "1.11.1" "@webassemblyjs/helper-wasm-bytecode" "1.11.1" +"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" + integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ast@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" @@ -6725,6 +6749,11 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f" integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== + "@webassemblyjs/floating-point-hex-parser@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz#3c3d3b271bddfc84deb00f71344438311d52ffb4" @@ -6735,6 +6764,11 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16" integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== + "@webassemblyjs/helper-api-error@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz#203f676e333b96c9da2eeab3ccef33c45928b6a2" @@ -6745,6 +6779,11 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5" integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== +"@webassemblyjs/helper-buffer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" + integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== + "@webassemblyjs/helper-buffer@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz#a1442d269c5feb23fcbc9ef759dac3547f29de00" @@ -6778,11 +6817,25 @@ "@webassemblyjs/helper-api-error" "1.11.1" "@xtuc/long" "4.2.2" +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@xtuc/long" "4.2.2" + "@webassemblyjs/helper-wasm-bytecode@1.11.1": version "1.11.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1" integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== + "@webassemblyjs/helper-wasm-bytecode@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz#4fed8beac9b8c14f8c58b70d124d549dd1fe5790" @@ -6798,6 +6851,16 @@ "@webassemblyjs/helper-wasm-bytecode" "1.11.1" "@webassemblyjs/wasm-gen" "1.11.1" +"@webassemblyjs/helper-wasm-section@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" + integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/helper-wasm-section@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz#5a4138d5a6292ba18b04c5ae49717e4167965346" @@ -6815,6 +6878,13 @@ dependencies: "@xtuc/ieee754" "^1.2.0" +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== + dependencies: + "@xtuc/ieee754" "^1.2.0" + "@webassemblyjs/ieee754@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz#15c7a0fbaae83fb26143bbacf6d6df1702ad39e4" @@ -6829,6 +6899,13 @@ dependencies: "@xtuc/long" "4.2.2" +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== + dependencies: + "@xtuc/long" "4.2.2" + "@webassemblyjs/leb128@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.9.0.tgz#f19ca0b76a6dc55623a09cffa769e838fa1e1c95" @@ -6841,6 +6918,11 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff" integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + "@webassemblyjs/utf8@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.9.0.tgz#04d33b636f78e6a6813227e82402f7637b6229ab" @@ -6874,6 +6956,20 @@ "@webassemblyjs/wasm-parser" "1.9.0" "@webassemblyjs/wast-printer" "1.9.0" +"@webassemblyjs/wasm-edit@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" + integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-opt" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + "@webassemblyjs/wast-printer" "1.11.6" + "@webassemblyjs/wasm-gen@1.11.1": version "1.11.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76" @@ -6885,6 +6981,17 @@ "@webassemblyjs/leb128" "1.11.1" "@webassemblyjs/utf8" "1.11.1" +"@webassemblyjs/wasm-gen@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" + integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + "@webassemblyjs/wasm-gen@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz#50bc70ec68ded8e2763b01a1418bf43491a7a49c" @@ -6906,6 +7013,16 @@ "@webassemblyjs/wasm-gen" "1.11.1" "@webassemblyjs/wasm-parser" "1.11.1" +"@webassemblyjs/wasm-opt@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" + integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + "@webassemblyjs/wasm-opt@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz#2211181e5b31326443cc8112eb9f0b9028721a61" @@ -6928,6 +7045,18 @@ "@webassemblyjs/leb128" "1.11.1" "@webassemblyjs/utf8" "1.11.1" +"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" + integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + "@webassemblyjs/wasm-parser@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz#9d48e44826df4a6598294aa6c87469d642fff65e" @@ -6960,6 +7089,14 @@ "@webassemblyjs/ast" "1.11.1" "@xtuc/long" "4.2.2" +"@webassemblyjs/wast-printer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" + integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@xtuc/long" "4.2.2" + "@webassemblyjs/wast-printer@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz#4935d54c85fef637b00ce9f52377451d00d47899" @@ -13528,6 +13665,14 @@ enhanced-resolve@^5.10.0, enhanced-resolve@^5.3.2: graceful-fs "^4.2.4" tapable "^2.2.0" +enhanced-resolve@^5.15.0: + version "5.15.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" + integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + enhanced-resolve@^5.8.0: version "5.12.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634" @@ -13665,7 +13810,7 @@ es-module-lexer@^0.9.0: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== -es-module-lexer@^1.3.0: +es-module-lexer@^1.2.1, es-module-lexer@^1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.1.tgz#c1b0dd5ada807a3b3155315911f364dc4e909db1" integrity sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q== @@ -18550,7 +18695,7 @@ jest-worker@^26.2.1, jest-worker@^26.3.0: merge-stream "^2.0.0" supports-color "^7.0.0" -jest-worker@^27.0.2, jest-worker@^27.0.6, jest-worker@^27.5.1: +jest-worker@^27.0.2, jest-worker@^27.0.6, jest-worker@^27.4.5, jest-worker@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== @@ -26890,6 +27035,15 @@ schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" +schema-utils@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + schema-utils@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.0.0.tgz#60331e9e3ae78ec5d16353c467c34b3a0a1d3df7" @@ -27008,6 +27162,13 @@ serialize-javascript@^6.0.0: dependencies: randombytes "^2.1.0" +serialize-javascript@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" + integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== + dependencies: + randombytes "^2.1.0" + serve-index@^1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" @@ -28849,6 +29010,17 @@ terser-webpack-plugin@^5.1.3: source-map "^0.6.1" terser "^5.7.2" +terser-webpack-plugin@^5.3.7: + version "5.3.9" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" + integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.17" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.16.8" + terser@5.14.2: version "5.14.2" resolved "https://registry.yarnpkg.com/terser/-/terser-5.14.2.tgz#9ac9f22b06994d736174f4091aa368db896f1c10" @@ -28886,6 +29058,16 @@ terser@^5.0.0, terser@^5.10.0, terser@^5.7.2: source-map "~0.7.2" source-map-support "~0.5.20" +terser@^5.16.8: + version "5.22.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.22.0.tgz#4f18103f84c5c9437aafb7a14918273310a8a49d" + integrity sha512-hHZVLgRA2z4NWcN6aS5rQDc+7Dcy58HOf2zbYwmFcQ+ua3h6eEFf5lIDKTzbWwlazPyOZsFQO8V80/IjVNExEw== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + terser@^5.7.0: version "5.16.3" resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.3.tgz#3266017a9b682edfe019b8ecddd2abaae7b39c6b" @@ -30879,6 +31061,36 @@ webpack@^5.52.0, webpack@~5.74.0: watchpack "^2.4.0" webpack-sources "^3.2.3" +webpack@^5.88.2: + version "5.89.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.89.0.tgz#56b8bf9a34356e93a6625770006490bf3a7f32dc" + integrity sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^1.0.0" + "@webassemblyjs/ast" "^1.11.5" + "@webassemblyjs/wasm-edit" "^1.11.5" + "@webassemblyjs/wasm-parser" "^1.11.5" + acorn "^8.7.1" + acorn-import-assertions "^1.9.0" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.15.0" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.9" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.2.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.7" + watchpack "^2.4.0" + webpack-sources "^3.2.3" + websocket-driver@0.6.5: version "0.6.5" resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36" From 8d5c306198a9f55665522898991c826b56992356 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 19 Oct 2023 10:57:26 +0200 Subject: [PATCH 12/38] test(loader): Improve loader tests & update loader (#9245) This updates the loader based on this PR: https://github.com/getsentry/sentry/pull/58070 and also actually fixes/improves the loader tests by avoiding importing `@sentry/browser`. This is not needed and actually obfuscates the "real"/"normal" behavior (just look at the generated `dist` folder for a test. Instead, I added an eslint rule to allow `Sentry` as a global for these cases, and just access them directly. This also uncovered an incorrect test, which I adjusted so it works. I also added a new test to ensure custom config works. --- .prettierignore | 1 + .../browser-integration-tests/.eslintrc.js | 8 +++++ .../fixtures/loader.js | 2 +- .../loader-suites/loader/noOnLoad/init.js | 2 -- .../noOnLoad/pageloadTransaction/init.js | 3 -- .../loader/noOnLoad/replay/init.js | 2 -- .../noOnLoad/sdkLoadedInMeanwhile/subject.js | 2 +- .../onLoad/captureExceptionInOnLoad/init.js | 6 ++-- .../onLoad/captureExceptionInOnLoad/test.ts | 8 +++++ .../onLoad/customBrowserTracing/init.js | 3 -- .../loader/onLoad/customInit/init.js | 20 +++++++++++ .../loader/onLoad/customInit/test.ts | 34 +++++++++++++++++++ .../loader/onLoad/customIntegrations/init.js | 4 --- .../onLoad/customIntegrationsFunction/init.js | 4 --- .../loader/onLoad/customReplay/init.js | 4 --- .../loader-suites/loader/onLoad/init.js | 4 --- .../loader/onLoad/onLoadLate/init.js | 0 .../loader/onLoad/onLoadLate/subject.js | 7 ++++ .../loader/onLoad/onLoadLate/test.ts | 21 ++++++++++++ .../loader/onLoad/pageloadTransaction/init.js | 3 -- .../loader/onLoad/replay/init.js | 4 --- 21 files changed, 103 insertions(+), 39 deletions(-) create mode 100644 packages/browser-integration-tests/loader-suites/loader/onLoad/customInit/init.js create mode 100644 packages/browser-integration-tests/loader-suites/loader/onLoad/customInit/test.ts create mode 100644 packages/browser-integration-tests/loader-suites/loader/onLoad/onLoadLate/init.js create mode 100644 packages/browser-integration-tests/loader-suites/loader/onLoad/onLoadLate/subject.js create mode 100644 packages/browser-integration-tests/loader-suites/loader/onLoad/onLoadLate/test.ts diff --git a/.prettierignore b/.prettierignore index f2ddf012c98e..f616ea97e557 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,3 @@ *.md .nxcache +packages/browser-integration-tests/fixtures/loader.js diff --git a/packages/browser-integration-tests/.eslintrc.js b/packages/browser-integration-tests/.eslintrc.js index 47e485f9068a..a19cfba8812a 100644 --- a/packages/browser-integration-tests/.eslintrc.js +++ b/packages/browser-integration-tests/.eslintrc.js @@ -13,6 +13,14 @@ module.exports = { 'fixtures/**', 'tmp/**', ], + overrides: [ + { + files: ['loader-suites/**/{subject,init}.js'], + globals: { + Sentry: true, + }, + }, + ], parserOptions: { sourceType: 'module', }, diff --git a/packages/browser-integration-tests/fixtures/loader.js b/packages/browser-integration-tests/fixtures/loader.js index 5ae0a94ebda8..a6fa24465a4f 100644 --- a/packages/browser-integration-tests/fixtures/loader.js +++ b/packages/browser-integration-tests/fixtures/loader.js @@ -1,4 +1,4 @@ -!function(e,n,r,t,i,o,a,c,s){for(var f=s,forceLoad=!1,u=0;u-1){f&&"no"===document.scripts[u].getAttribute("data-lazy")&&(f=!1);break}var p=!1,d=[],l=function(e){("e"in e||"p"in e||e.f&&e.f.indexOf("capture")>-1||e.f&&e.f.indexOf("showReportDialog")>-1)&&f&&h(d),l.data.push(e)};function _(){l({e:[].slice.call(arguments)})}function v(e){l({p:"reason"in e?e.reason:"detail"in e&&"reason"in e.detail?e.detail.reason:e})}function h(o){if(!p){p=!0;var s=n.scripts[0],f=n.createElement("script");f.src=a,f.crossOrigin="anonymous",f.addEventListener("load",(function(){try{e.removeEventListener(r,_),e.removeEventListener(t,v),e.SENTRY_SDK_SOURCE="loader";var n=e[i],a=n.init;n.init=function(e){var r=c;for(var t in e)Object.prototype.hasOwnProperty.call(e,t)&&(r[t]=e[t]);!function(e,n){var r=e.integrations||[];if(!Array.isArray(r))return;var t=r.map((function(e){return e.name}));e.tracesSampleRate&&-1===t.indexOf("BrowserTracing")&&r.push(new n.BrowserTracing);(e.replaysSessionSampleRate||e.replaysOnErrorSampleRate)&&-1===t.indexOf("Replay")&&r.push(new n.Replay);e.integrations=r}(r,n),a(r)},function(n,r){try{for(var t=0;t-1){s&&"no"===document.scripts[f].getAttribute("data-lazy")&&(s=!1);break}var p=[];function l(n){return"e"in n}function d(n){return"p"in n}function _(n){return"f"in n}var v=[];function h(n){s&&(l(n)||d(n)||_(n)&&n.f.indexOf("capture")>-1||_(n)&&n.f.indexOf("showReportDialog")>-1)&&O(),v.push(n)}function y(){h({e:[].slice.call(arguments)})}function E(n){h({p:"reason"in n?n.reason:"detail"in n&&"reason"in n.detail?n.detail.reason:n})}function m(){try{n.SENTRY_SDK_SOURCE="loader";var e=n[i],o=e.init;e.init=function(i){n.removeEventListener(r,y),n.removeEventListener(t,E);var a=c;for(var u in i)Object.prototype.hasOwnProperty.call(i,u)&&(a[u]=i[u]);!function(n,e){var r=n.integrations||[];if(!Array.isArray(r))return;var t=r.map((function(n){return n.name}));n.tracesSampleRate&&-1===t.indexOf("BrowserTracing")&&r.push(new e.BrowserTracing);(n.replaysSessionSampleRate||n.replaysOnErrorSampleRate)&&-1===t.indexOf("Replay")&&r.push(new e.Replay);n.integrations=r}(a,e),o(a)},setTimeout((function(){return function(e){try{for(var r=0;r { cdnScript.src = '/cdn.bundle.js'; cdnScript.addEventListener('load', () => { - window.Sentry.init({ + Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', replaysSessionSampleRate: 0.42, }); diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/captureExceptionInOnLoad/init.js b/packages/browser-integration-tests/loader-suites/loader/onLoad/captureExceptionInOnLoad/init.js index 71a703f2f9e1..8c8c99e30367 100644 --- a/packages/browser-integration-tests/loader-suites/loader/onLoad/captureExceptionInOnLoad/init.js +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/captureExceptionInOnLoad/init.js @@ -1,7 +1,5 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; - Sentry.onLoad(function () { + // You _have_ to call Sentry.init() before calling Sentry.captureException() in Sentry.onLoad()! + Sentry.init(); Sentry.captureException('Test exception'); }); diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/captureExceptionInOnLoad/test.ts b/packages/browser-integration-tests/loader-suites/loader/onLoad/captureExceptionInOnLoad/test.ts index b63a8d6db1e4..b3cf79bde5c2 100644 --- a/packages/browser-integration-tests/loader-suites/loader/onLoad/captureExceptionInOnLoad/test.ts +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/captureExceptionInOnLoad/test.ts @@ -4,6 +4,14 @@ import { sentryTest } from '../../../../utils/fixtures'; import { envelopeRequestParser, waitForErrorRequestOnUrl } from '../../../../utils/helpers'; sentryTest('captureException works inside of onLoad', async ({ getLocalTestUrl, page }) => { + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + const url = await getLocalTestUrl({ testDir: __dirname }); const req = await waitForErrorRequestOnUrl(page, url); diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/customBrowserTracing/init.js b/packages/browser-integration-tests/loader-suites/loader/onLoad/customBrowserTracing/init.js index cf30bc698e53..269593b620f1 100644 --- a/packages/browser-integration-tests/loader-suites/loader/onLoad/customBrowserTracing/init.js +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/customBrowserTracing/init.js @@ -1,6 +1,3 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; window._testBaseTimestamp = performance.timeOrigin / 1000; Sentry.onLoad(function () { diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/customInit/init.js b/packages/browser-integration-tests/loader-suites/loader/onLoad/customInit/init.js new file mode 100644 index 000000000000..4a9e000fd1c2 --- /dev/null +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/customInit/init.js @@ -0,0 +1,20 @@ +window.__sentryOnLoad = 0; + +setTimeout(() => { + Sentry.onLoad(function () { + window.__hadSentry = window.sentryIsLoaded(); + + Sentry.init({ + sampleRate: 0.5, + }); + + window.__sentryOnLoad++; + }); +}); + +window.sentryIsLoaded = () => { + const __sentry = window.__SENTRY__; + + // If there is a global __SENTRY__ that means that in any of the callbacks init() was already invoked + return !!(!(typeof __sentry === 'undefined') && __sentry.hub && __sentry.hub.getClient()); +}; diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/customInit/test.ts b/packages/browser-integration-tests/loader-suites/loader/onLoad/customInit/test.ts new file mode 100644 index 000000000000..eeea9de48fb2 --- /dev/null +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/customInit/test.ts @@ -0,0 +1,34 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { LOADER_CONFIGS } from '../../../../utils/generatePlugin'; + +const bundle = process.env.PW_BUNDLE || ''; +const isLazy = LOADER_CONFIGS[bundle]?.lazy; + +sentryTest('always calls onLoad init correctly', async ({ getLocalTestUrl, page }) => { + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + + // We want to test that if we are _not_ lazy, we are correctly calling onLoad init() + // But if we are lazy and call `forceLoad`, we also call the onLoad init() correctly + if (isLazy) { + expect(await page.evaluate('window.__sentryOnLoad')).toEqual(0); + await page.evaluate('Sentry.forceLoad()'); + } + + await page.waitForFunction('window.__sentryOnLoad && window.sentryIsLoaded()'); + + expect(await page.evaluate('window.__hadSentry')).toEqual(false); + expect(await page.evaluate('window.__sentryOnLoad')).toEqual(1); + expect(await page.evaluate('Sentry.getCurrentHub().getClient().getOptions().sampleRate')).toEqual(0.5); +}); diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrations/init.js b/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrations/init.js index a5440c1979c5..5d2920680cfc 100644 --- a/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrations/init.js +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrations/init.js @@ -1,7 +1,3 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; - class CustomIntegration { constructor() { this.name = 'CustomIntegration'; diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrationsFunction/init.js b/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrationsFunction/init.js index 4c1e600794d5..0836f8b3b887 100644 --- a/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrationsFunction/init.js +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrationsFunction/init.js @@ -1,7 +1,3 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; - class CustomIntegration { constructor() { this.name = 'CustomIntegration'; diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/customReplay/init.js b/packages/browser-integration-tests/loader-suites/loader/onLoad/customReplay/init.js index 64d2463ed668..921209ce14dc 100644 --- a/packages/browser-integration-tests/loader-suites/loader/onLoad/customReplay/init.js +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/customReplay/init.js @@ -1,7 +1,3 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; - Sentry.onLoad(function () { Sentry.init({ integrations: [ diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/init.js b/packages/browser-integration-tests/loader-suites/loader/onLoad/init.js index 5a9398da8d47..e63705186b2f 100644 --- a/packages/browser-integration-tests/loader-suites/loader/onLoad/init.js +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/init.js @@ -1,7 +1,3 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; - Sentry.onLoad(function () { Sentry.init({}); }); diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/onLoadLate/init.js b/packages/browser-integration-tests/loader-suites/loader/onLoad/onLoadLate/init.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/onLoadLate/subject.js b/packages/browser-integration-tests/loader-suites/loader/onLoad/onLoadLate/subject.js new file mode 100644 index 000000000000..1dcef58798cc --- /dev/null +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/onLoadLate/subject.js @@ -0,0 +1,7 @@ +Sentry.forceLoad(); + +setTimeout(() => { + Sentry.onLoad(function () { + Sentry.captureException('Test exception'); + }); +}, 200); diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/onLoadLate/test.ts b/packages/browser-integration-tests/loader-suites/loader/onLoad/onLoadLate/test.ts new file mode 100644 index 000000000000..46bbf81f3c58 --- /dev/null +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/onLoadLate/test.ts @@ -0,0 +1,21 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, waitForErrorRequestOnUrl } from '../../../../utils/helpers'; + +sentryTest('late onLoad call is handled', async ({ getLocalTestUrl, page }) => { + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname }); + const req = await waitForErrorRequestOnUrl(page, url); + + const eventData = envelopeRequestParser(req); + + expect(eventData.message).toBe('Test exception'); +}); diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/pageloadTransaction/init.js b/packages/browser-integration-tests/loader-suites/loader/onLoad/pageloadTransaction/init.js index dcb6c3e90d0d..7c0fceed58a4 100644 --- a/packages/browser-integration-tests/loader-suites/loader/onLoad/pageloadTransaction/init.js +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/pageloadTransaction/init.js @@ -1,6 +1,3 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; window._testBaseTimestamp = performance.timeOrigin / 1000; Sentry.onLoad(function () { diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/replay/init.js b/packages/browser-integration-tests/loader-suites/loader/onLoad/replay/init.js index 5a9398da8d47..e63705186b2f 100644 --- a/packages/browser-integration-tests/loader-suites/loader/onLoad/replay/init.js +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/replay/init.js @@ -1,7 +1,3 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; - Sentry.onLoad(function () { Sentry.init({}); }); From 2ff6bd333ad8a0bf9527b69e9612101cef11c427 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 19 Oct 2023 11:59:07 +0200 Subject: [PATCH 13/38] test(deno): Fix runtime normalization (#9309) --- packages/deno/test/__snapshots__/mod.test.ts.snap | 4 ++-- packages/deno/test/normalize.ts | 9 +++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/deno/test/__snapshots__/mod.test.ts.snap b/packages/deno/test/__snapshots__/mod.test.ts.snap index aa541d9679b9..2cdb246a8e8b 100644 --- a/packages/deno/test/__snapshots__/mod.test.ts.snap +++ b/packages/deno/test/__snapshots__/mod.test.ts.snap @@ -15,7 +15,7 @@ snapshot[`captureException 1`] = ` }, runtime: { name: "deno", - version: "1.37.1", + version: "{{version}}", }, trace: { span_id: "{{id}}", @@ -176,7 +176,7 @@ snapshot[`captureMessage 1`] = ` }, runtime: { name: "deno", - version: "1.37.1", + version: "{{version}}", }, trace: { span_id: "{{id}}", diff --git a/packages/deno/test/normalize.ts b/packages/deno/test/normalize.ts index ad7081c52efe..45f631116955 100644 --- a/packages/deno/test/normalize.ts +++ b/packages/deno/test/normalize.ts @@ -90,12 +90,9 @@ function normalizeEvent(event: any): any { event.contexts.v8.version = '{{version}}'; } - if (event.contexts?.deno) { - if (event.contexts.deno?.version) { - event.contexts.deno.version = '{{version}}'; - } - if (event.contexts.deno?.target) { - event.contexts.deno.target = '{{target}}'; + if (event.contexts?.runtime) { + if (event.contexts.runtime?.version) { + event.contexts.runtime.version = '{{version}}'; } } From 45d51f6bc5e548ff374141f9e55a788814a3b158 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 19 Oct 2023 14:44:35 +0200 Subject: [PATCH 14/38] meta(deno): Add deno craft release target (#9310) --- .craft.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.craft.yml b/.craft.yml index 521e1e4f8ba3..ac8236c36c1c 100644 --- a/.craft.yml +++ b/.craft.yml @@ -70,6 +70,13 @@ targets: - name: npm id: '@sentry/deno' includeNames: /^sentry-deno-\d.*\.tgz$/ + - name: commit-on-git-repository + # This will publish on the Deno registry + archive: /^sentry-deno-\d.*\.tgz$/ + repositoryUrl: git@github.com:getsentry/sentry-deno.git + stripComponents: 1 + branch: main + createTag: true ## 5. Node-based Packages - name: npm From d7df0986bab3be2baf2ca75b87538a063c444ff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Wold=C5=99ich?= <31292499+krystofwoldrich@users.noreply.github.com> Date: Thu, 19 Oct 2023 15:25:41 +0200 Subject: [PATCH 15/38] feat(types): Add missing Profiling types (macho debug image, profile measurements, stack frame properties) (#9277) --- packages/types/src/debugMeta.ts | 10 +++++++++- packages/types/src/profiling.ts | 19 ++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/types/src/debugMeta.ts b/packages/types/src/debugMeta.ts index da3cf958857b..44ada6fd0e86 100644 --- a/packages/types/src/debugMeta.ts +++ b/packages/types/src/debugMeta.ts @@ -5,7 +5,7 @@ export interface DebugMeta { images?: Array; } -export type DebugImage = WasmDebugImage | SourceMapDebugImage; +export type DebugImage = WasmDebugImage | SourceMapDebugImage | MachoDebugImage; interface WasmDebugImage { type: 'wasm'; @@ -20,3 +20,11 @@ interface SourceMapDebugImage { code_file: string; // filename debug_id: string; // uuid } + +interface MachoDebugImage { + type: 'macho'; + debug_id: string; + image_addr: string; + image_size?: number; + code_file?: string; +} diff --git a/packages/types/src/profiling.ts b/packages/types/src/profiling.ts index 84a5238e7ace..3650500fcd7b 100644 --- a/packages/types/src/profiling.ts +++ b/packages/types/src/profiling.ts @@ -1,4 +1,6 @@ import type { DebugImage } from './debugMeta'; +import type { MeasurementUnit } from './measurement'; + export type ThreadId = string; export type FrameId = number; export type StackId = number; @@ -6,17 +8,22 @@ export type StackId = number; export interface ThreadCpuSample { stack_id: StackId; thread_id: ThreadId; + queue_address?: string; elapsed_since_start_ns: string; } export type ThreadCpuStack = FrameId[]; export type ThreadCpuFrame = { - function: string; + function?: string; file?: string; lineno?: number; colno?: number; abs_path?: string; + platform?: string; + instruction_addr?: string; + module?: string; + in_app?: boolean; }; export interface ThreadCpuProfile { @@ -68,4 +75,14 @@ export interface Profile { relative_start_ns: string; relative_end_ns: string; }[]; + measurements?: Record< + string, + { + unit: MeasurementUnit; + values: { + elapsed_since_start_ns: number; + value: number; + }[]; + } + >; } From faf07bbdfa7ebd42435ac8d50faf2d28b1528392 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 19 Oct 2023 17:13:56 +0200 Subject: [PATCH 16/38] fix(e2e): Resolve `@sentry/node` dependency in bundler plugins to current version (#9313) --- .../test-applications/debug-id-sourcemaps/package.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json b/packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json index b261ace67fb8..ec6eeba0066c 100644 --- a/packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json +++ b/packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json @@ -17,6 +17,12 @@ "vitest": "^0.34.6", "@sentry/rollup-plugin": "2.8.0" }, + "pnpm": { + "overrides": { + "@sentry/node": "latest || *", + "@sentry/utils": "latest || *" + } + }, "volta": { "extends": "../../package.json" } From b92532291676159d9c6ab241676c535238f484ed Mon Sep 17 00:00:00 2001 From: Subash-Lamichhane <109226874+Subash-Lamichhane@users.noreply.github.com> Date: Thu, 19 Oct 2023 21:15:29 +0545 Subject: [PATCH 17/38] chore: Fix typos (#9315) --- CONTRIBUTING.md | 2 +- MIGRATION.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9048da5efddf..9af7a880a407 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -132,7 +132,7 @@ Additional labels for categorization can be added, and the Sentry SDK team may a ### Pull Requests (PRs) PRs are merged via `Squash and merge`. -This means that all commits on the branch will be squashed into a single commit, and commited as such onto master. +This means that all commits on the branch will be squashed into a single commit, and committed as such onto master. * The PR name can generally follow the commit name (e.g. `feat(core): Set custom transaction source for event processors`) * Make sure to rebase the branch on `master` before squashing it diff --git a/MIGRATION.md b/MIGRATION.md index 6dc1886f7174..7f626efe7e23 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -100,7 +100,7 @@ Sentry.init({ ## Replay options changed (since 7.35.0) - #6645 -Some options for replay have been depracted in favor of new APIs. +Some options for replay have been deprecated in favor of new APIs. See [Replay Migration docs](./packages/replay/MIGRATION.md#upgrading-replay-from-7340-to-7350) for details. ## Renaming of Next.js wrapper methods (since 7.31.0) - #6790 @@ -162,7 +162,7 @@ Running the new SDK version on Node.js v6 is therefore highly discouraged. ## Removal of `@sentry/minimal` -The `@sentry/minimal` package was deleted and it's functionality was moved to `@sentry/hub`. All exports from `@sentry/minimal` should be avaliable in `@sentry/hub` other than `_callOnClient` function which was removed. +The `@sentry/minimal` package was deleted and it's functionality was moved to `@sentry/hub`. All exports from `@sentry/minimal` should be available in `@sentry/hub` other than `_callOnClient` function which was removed. ```ts // New in v7: @@ -184,7 +184,7 @@ import { ## Explicit Client Options -In v7, we've updated the `Client` to have options seperate from the options passed into `Sentry.init`. This means that constructing a client now requires 3 options: `integrations`, `transport` and `stackParser`. These can be customized as you see fit. +In v7, we've updated the `Client` to have options separate from the options passed into `Sentry.init`. This means that constructing a client now requires 3 options: `integrations`, `transport` and `stackParser`. These can be customized as you see fit. ```ts import { BrowserClient, defaultStackParser, defaultIntegrations, makeFetchTransport } from '@sentry/browser'; @@ -764,7 +764,7 @@ this case is the `event_id`, in case the event will not be sent because of filte In `4.x` we had both `close` and `flush` on the `Client` draining the internal queue of events, helpful when you were using `@sentry/node` on a serverless infrastructure. -Now `close` and `flush` work similar, with the difference that if you call `close` in addition to returing a `Promise` +Now `close` and `flush` work similar, with the difference that if you call `close` in addition to returning a `Promise` that you can await it also **disables** the client so it will not send any future events. # Migrating from `raven-js` to `@sentry/browser` From 1c2b829e41c98edf0d972fab381d06ee1bfc21f2 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 19 Oct 2023 12:07:52 -0400 Subject: [PATCH 18/38] fix(node): Check buffer length when attempting to parse ANR frame (#9314) --- packages/node/src/anr/websocket.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/node/src/anr/websocket.ts b/packages/node/src/anr/websocket.ts index 9faa90bcfd1c..7229f0fc07e7 100644 --- a/packages/node/src/anr/websocket.ts +++ b/packages/node/src/anr/websocket.ts @@ -296,6 +296,13 @@ class WebSocketInterface extends EventEmitter { return; } + // There needs to be atleast two values in the buffer for us to parse + // a frame from it. + // See: https://github.com/getsentry/sentry-javascript/issues/9307 + if (buff.length <= 1) { + return; + } + const frame = parseFrame(buff); if (isCompleteFrame(frame)) { From 7abd153692e61fbe5a869cae109cbacdcc5a89f8 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 19 Oct 2023 12:42:46 -0400 Subject: [PATCH 19/38] feat(types): Add statsd envelope types (#9304) --- packages/types/src/datacategory.ts | 4 +++- packages/types/src/envelope.ts | 7 ++++++- packages/utils/src/envelope.ts | 2 ++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/types/src/datacategory.ts b/packages/types/src/datacategory.ts index 06f64c8525bb..84340cffc4f1 100644 --- a/packages/types/src/datacategory.ts +++ b/packages/types/src/datacategory.ts @@ -23,4 +23,6 @@ export type DataCategory = // Profile event type | 'profile' // Check-in event (monitor) - | 'monitor'; + | 'monitor' + // Unknown data category + | 'unknown'; diff --git a/packages/types/src/envelope.ts b/packages/types/src/envelope.ts index 288453fc4fdb..a039874a2d54 100644 --- a/packages/types/src/envelope.ts +++ b/packages/types/src/envelope.ts @@ -34,7 +34,8 @@ export type EnvelopeItemType = | 'profile' | 'replay_event' | 'replay_recording' - | 'check_in'; + | 'check_in' + | 'statsd'; export type BaseEnvelopeHeaders = { [key: string]: unknown; @@ -72,6 +73,7 @@ type ClientReportItemHeaders = { type: 'client_report' }; type ReplayEventItemHeaders = { type: 'replay_event' }; type ReplayRecordingItemHeaders = { type: 'replay_recording'; length: number }; type CheckInItemHeaders = { type: 'check_in' }; +type StatsdItemHeaders = { type: 'statsd' }; export type EventItem = BaseEnvelopeItem; export type AttachmentItem = BaseEnvelopeItem; @@ -84,18 +86,21 @@ export type ClientReportItem = BaseEnvelopeItem; type ReplayEventItem = BaseEnvelopeItem; type ReplayRecordingItem = BaseEnvelopeItem; +export type StatsdItem = BaseEnvelopeItem; export type EventEnvelopeHeaders = { event_id: string; sent_at: string; trace?: DynamicSamplingContext }; type SessionEnvelopeHeaders = { sent_at: string }; type CheckInEnvelopeHeaders = { trace?: DynamicSamplingContext }; type ClientReportEnvelopeHeaders = BaseEnvelopeHeaders; type ReplayEnvelopeHeaders = BaseEnvelopeHeaders; +type StatsdEnvelopeHeaders = BaseEnvelopeHeaders; export type EventEnvelope = BaseEnvelope; export type SessionEnvelope = BaseEnvelope; export type ClientReportEnvelope = BaseEnvelope; export type ReplayEnvelope = [ReplayEnvelopeHeaders, [ReplayEventItem, ReplayRecordingItem]]; export type CheckInEvelope = BaseEnvelope; +export type StatsdEnvelope = BaseEnvelope; export type Envelope = EventEnvelope | SessionEnvelope | ClientReportEnvelope | ReplayEnvelope | CheckInEvelope; export type EnvelopeItem = Envelope[1][number]; diff --git a/packages/utils/src/envelope.ts b/packages/utils/src/envelope.ts index e249564eca51..54772c6ae6fc 100644 --- a/packages/utils/src/envelope.ts +++ b/packages/utils/src/envelope.ts @@ -208,6 +208,8 @@ const ITEM_TYPE_TO_DATA_CATEGORY_MAP: Record = { replay_event: 'replay', replay_recording: 'replay', check_in: 'monitor', + // TODO: This is a temporary workaround until we have a proper data category for metrics + statsd: 'unknown', }; /** From 7236510474b7c3f4821e9ebe74207b8566f75f60 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 19 Oct 2023 19:04:35 +0200 Subject: [PATCH 20/38] feat(node): Vendor `cookie` module (#9308) --- packages/node/package.json | 1 - packages/node/src/cookie.ts | 78 +++++++++++++++++++++++++++++++ packages/node/src/requestdata.ts | 6 +-- packages/node/test/cookie.test.ts | 67 ++++++++++++++++++++++++++ 4 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 packages/node/src/cookie.ts create mode 100644 packages/node/test/cookie.test.ts diff --git a/packages/node/package.json b/packages/node/package.json index 6f0da4a58815..ff847f28a518 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -27,7 +27,6 @@ "@sentry/core": "7.74.1", "@sentry/types": "7.74.1", "@sentry/utils": "7.74.1", - "cookie": "^0.5.0", "https-proxy-agent": "^5.0.0" }, "devDependencies": { diff --git a/packages/node/src/cookie.ts b/packages/node/src/cookie.ts new file mode 100644 index 000000000000..2a9d21654ba6 --- /dev/null +++ b/packages/node/src/cookie.ts @@ -0,0 +1,78 @@ +/** + * This code was originally copied from the 'cookie` module at v0.5.0 and was simplified for our use case. + * https://github.com/jshttp/cookie/blob/a0c84147aab6266bdb3996cf4062e93907c0b0fc/index.js + * It had the following license: + * + * (The MIT License) + * + * Copyright (c) 2012-2014 Roman Shtylman + * Copyright (c) 2015 Douglas Christopher Wilson + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * 'Software'), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/** + * Parses a cookie string + */ +export function parseCookie(str: string): Record { + const obj: Record = {}; + let index = 0; + + while (index < str.length) { + const eqIdx = str.indexOf('=', index); + + // no more cookie pairs + if (eqIdx === -1) { + break; + } + + let endIdx = str.indexOf(';', index); + + if (endIdx === -1) { + endIdx = str.length; + } else if (endIdx < eqIdx) { + // backtrack on prior semicolon + index = str.lastIndexOf(';', eqIdx - 1) + 1; + continue; + } + + const key = str.slice(index, eqIdx).trim(); + + // only assign once + if (undefined === obj[key]) { + let val = str.slice(eqIdx + 1, endIdx).trim(); + + // quoted values + if (val.charCodeAt(0) === 0x22) { + val = val.slice(1, -1); + } + + try { + obj[key] = val.indexOf('%') !== -1 ? decodeURIComponent(val) : val; + } catch (e) { + obj[key] = val; + } + } + + index = endIdx + 1; + } + + return obj; +} diff --git a/packages/node/src/requestdata.ts b/packages/node/src/requestdata.ts index e746db088a95..4d464ba13825 100644 --- a/packages/node/src/requestdata.ts +++ b/packages/node/src/requestdata.ts @@ -6,9 +6,10 @@ import type { TransactionSource, } from '@sentry/types'; import { isPlainObject, isString, normalize, stripUrlQueryAndFragment } from '@sentry/utils'; -import * as cookie from 'cookie'; import * as url from 'url'; +import { parseCookie } from './cookie'; + const DEFAULT_INCLUDES = { ip: false, request: true, @@ -202,11 +203,10 @@ export function extractRequestData( // cookies: // node, express, koa: req.headers.cookie // vercel, sails.js, express (w/ cookie middleware), nextjs: req.cookies - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access requestData.cookies = // TODO (v8 / #5257): We're only sending the empty object for backwards compatibility, so the last bit can // come off in v8 - req.cookies || (headers.cookie && cookie.parse(headers.cookie)) || {}; + req.cookies || (headers.cookie && parseCookie(headers.cookie)) || {}; break; } case 'query_string': { diff --git a/packages/node/test/cookie.test.ts b/packages/node/test/cookie.test.ts new file mode 100644 index 000000000000..2110f384c9b6 --- /dev/null +++ b/packages/node/test/cookie.test.ts @@ -0,0 +1,67 @@ +/** + * This code was originally copied from the 'cookie` module at v0.5.0 and was simplified for our use case. + * https://github.com/jshttp/cookie/blob/a0c84147aab6266bdb3996cf4062e93907c0b0fc/test/parse.js + * It had the following license: + * + * (The MIT License) + * + * Copyright (c) 2012-2014 Roman Shtylman + * Copyright (c) 2015 Douglas Christopher Wilson + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * 'Software'), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { parseCookie } from '../src/cookie'; + +describe('parseCookie(str)', function () { + it('should parse cookie string to object', function () { + expect(parseCookie('foo=bar')).toEqual({ foo: 'bar' }); + expect(parseCookie('foo=123')).toEqual({ foo: '123' }); + }); + + it('should ignore OWS', function () { + expect(parseCookie('FOO = bar; baz = raz')).toEqual({ FOO: 'bar', baz: 'raz' }); + }); + + it('should parse cookie with empty value', function () { + expect(parseCookie('foo= ; bar=')).toEqual({ foo: '', bar: '' }); + }); + + it('should URL-decode values', function () { + expect(parseCookie('foo="bar=123456789&name=Magic+Mouse"')).toEqual({ foo: 'bar=123456789&name=Magic+Mouse' }); + + expect(parseCookie('email=%20%22%2c%3b%2f')).toEqual({ email: ' ",;/' }); + }); + + it('should return original value on escape error', function () { + expect(parseCookie('foo=%1;bar=bar')).toEqual({ foo: '%1', bar: 'bar' }); + }); + + it('should ignore cookies without value', function () { + expect(parseCookie('foo=bar;fizz ; buzz')).toEqual({ foo: 'bar' }); + expect(parseCookie(' fizz; foo= bar')).toEqual({ foo: 'bar' }); + }); + + it('should ignore duplicate cookies', function () { + expect(parseCookie('foo=%1;bar=bar;foo=boo')).toEqual({ foo: '%1', bar: 'bar' }); + expect(parseCookie('foo=false;bar=bar;foo=tre')).toEqual({ foo: 'false', bar: 'bar' }); + expect(parseCookie('foo=;bar=bar;foo=boo')).toEqual({ foo: '', bar: 'bar' }); + }); +}); From 7935e7869383a8a836ff318a7c8914a507c4df42 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Thu, 19 Oct 2023 15:01:02 -0400 Subject: [PATCH 21/38] feat(dev): Change `yalc:publish` to publish with a hash (#9269) Adds `--sig` option to `yalc:publish` tasks to include a hash of file contents as part of package version. This allows us to cache bust and allows dev servers to hot reload in watch mode. --- packages/angular-ivy/package.json | 2 +- packages/angular/package.json | 2 +- packages/astro/package.json | 2 +- packages/bun/package.json | 2 +- packages/core/package.json | 2 +- packages/gatsby/package.json | 2 +- packages/hub/package.json | 2 +- packages/nextjs/package.json | 2 +- packages/node-experimental/package.json | 2 +- packages/node/package.json | 2 +- packages/opentelemetry-node/package.json | 2 +- packages/react/package.json | 2 +- packages/remix/package.json | 2 +- packages/svelte/package.json | 2 +- packages/sveltekit/package.json | 2 +- packages/tracing-internal/package.json | 2 +- packages/types/package.json | 2 +- packages/utils/package.json | 2 +- packages/vercel-edge/package.json | 2 +- packages/vue/package.json | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/angular-ivy/package.json b/packages/angular-ivy/package.json index 3e273b183752..d1aa2d1d39ba 100644 --- a/packages/angular-ivy/package.json +++ b/packages/angular-ivy/package.json @@ -56,7 +56,7 @@ "lint": "run-s lint:prettier lint:eslint", "lint:eslint": "eslint . --format stylish", "lint:prettier": "prettier --check \"{src,test,scripts}/**/**.ts\"", - "yalc:publish": "yalc publish build --push" + "yalc:publish": "yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" diff --git a/packages/angular/package.json b/packages/angular/package.json index c45cd1df78d1..fbecb6918313 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -60,7 +60,7 @@ "test": "yarn test:unit", "test:unit": "jest", "test:unit:watch": "jest --watch", - "yalc:publish": "yalc publish build --push" + "yalc:publish": "yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" diff --git a/packages/astro/package.json b/packages/astro/package.json index bc5ebb828a95..192dbb6d2a1f 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -70,7 +70,7 @@ "test": "yarn test:unit", "test:unit": "vitest run", "test:watch": "vitest --watch", - "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" + "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" diff --git a/packages/bun/package.json b/packages/bun/package.json index 528cd44d1252..c33e1a106f14 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -55,7 +55,7 @@ "test": "run-s install:bun test:bun", "test:bun": "bun test", "test:watch": "bun test --watch", - "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" + "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" diff --git a/packages/core/package.json b/packages/core/package.json index 8f803c731e32..6d67923f6238 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -49,7 +49,7 @@ "test": "jest", "test:watch": "jest --watch", "version": "node ../../scripts/versionbump.js src/version.ts", - "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" + "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index b79089d40fc3..e2755ccb64ed 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -65,7 +65,7 @@ "lint:prettier": "prettier --check \"{src,test,scripts}/**/**.{ts,tsx,js}\"", "test": "yarn ts-node scripts/pretest.ts && yarn jest", "test:watch": "yarn ts-node scripts/pretest.ts && yarn jest --watch", - "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" + "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" diff --git a/packages/hub/package.json b/packages/hub/package.json index 4d6df0cd3f57..ecb1647d6ea9 100644 --- a/packages/hub/package.json +++ b/packages/hub/package.json @@ -49,7 +49,7 @@ "lint:prettier": "prettier --check \"{src,test,scripts}/**/**.ts\"", "test": "jest", "test:watch": "jest --watch", - "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" + "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 05aaa3f47cca..e68514a79e8b 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -86,7 +86,7 @@ "test:watch": "jest --watch", "vercel:branch": "source vercel/set-up-branch-for-test-app-use.sh", "vercel:project": "source vercel/make-project-use-current-branch.sh", - "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" + "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" diff --git a/packages/node-experimental/package.json b/packages/node-experimental/package.json index dd3a5bd3afee..39f226981173 100644 --- a/packages/node-experimental/package.json +++ b/packages/node-experimental/package.json @@ -71,7 +71,7 @@ "test": "yarn test:jest", "test:jest": "jest", "test:watch": "jest --watch", - "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" + "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" diff --git a/packages/node/package.json b/packages/node/package.json index ff847f28a518..73752472d47c 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -64,7 +64,7 @@ "test:release-health": "node test/manual/release-health/runner.js", "test:webpack": "cd test/manual/webpack-async-context/ && yarn --silent && node npm-build.js", "test:watch": "jest --watch", - "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" + "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" diff --git a/packages/opentelemetry-node/package.json b/packages/opentelemetry-node/package.json index f49b31fdee7a..9de6cc2d071a 100644 --- a/packages/opentelemetry-node/package.json +++ b/packages/opentelemetry-node/package.json @@ -64,7 +64,7 @@ "test": "yarn test:jest", "test:jest": "jest", "test:watch": "jest --watch", - "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" + "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" diff --git a/packages/react/package.json b/packages/react/package.json index cf33891c6585..1ff74ae150b6 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -78,7 +78,7 @@ "lint:prettier": "prettier --check \"{src,test,scripts}/**/**.ts\"", "test": "jest", "test:watch": "jest --watch", - "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" + "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" diff --git a/packages/remix/package.json b/packages/remix/package.json index 3d238a2bedfe..2a7e6f618c79 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -78,7 +78,7 @@ "test:integration:server": "export NODE_OPTIONS='--stack-trace-limit=25' && jest --config=test/integration/jest.config.js test/integration/test/server/", "test:unit": "jest", "test:watch": "jest --watch", - "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" + "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" diff --git a/packages/svelte/package.json b/packages/svelte/package.json index ff7515b6bf50..675e480ea4b8 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -58,7 +58,7 @@ "lint:prettier": "prettier --check \"{src,test,scripts}/**/**.ts\"", "test": "jest", "test:watch": "jest --watch", - "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" + "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 1cde8132cc03..ab0bcc70835f 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -58,7 +58,7 @@ "test": "yarn test:unit", "test:unit": "vitest run", "test:watch": "vitest --watch", - "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" + "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" diff --git a/packages/tracing-internal/package.json b/packages/tracing-internal/package.json index acbab9607598..f58679dd7ca7 100644 --- a/packages/tracing-internal/package.json +++ b/packages/tracing-internal/package.json @@ -52,7 +52,7 @@ "test:unit": "jest", "test": "jest", "test:watch": "jest --watch", - "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" + "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" diff --git a/packages/types/package.json b/packages/types/package.json index a0e518d14168..41f997c6652a 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -41,7 +41,7 @@ "fix": "run-s fix:eslint fix:prettier", "fix:eslint": "eslint . --format stylish --fix", "fix:prettier": "prettier --write \"{src,test,scripts}/**/**.ts\"", - "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" + "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" diff --git a/packages/utils/package.json b/packages/utils/package.json index 26a03c03f78a..d316e05a03e8 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -54,7 +54,7 @@ "test": "jest", "test:watch": "jest --watch", "test:package": "node test/types/index.js", - "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" + "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json index f8081015dcb3..2b9f97acef1f 100644 --- a/packages/vercel-edge/package.json +++ b/packages/vercel-edge/package.json @@ -53,7 +53,7 @@ "lint:prettier": "prettier --check \"{src,test,scripts}/**/**.ts\"", "test": "jest", "test:watch": "jest --watch", - "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" + "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" diff --git a/packages/vue/package.json b/packages/vue/package.json index ce1b5057c38e..af1d6a0de182 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -56,7 +56,7 @@ "lint:prettier": "prettier --check \"{src,test,scripts}/**/**.ts\"", "test": "jest", "test:watch": "jest --watch", - "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" + "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" From d7de20ebc31e495e870cd62282d56eba3faad25c Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Thu, 19 Oct 2023 15:36:12 -0400 Subject: [PATCH 22/38] ref: Move `isBrowser` util fn from `replay` to `util` (#9289) We need this in the new feedback integration. --- packages/replay/jest.setup.ts | 7 ++----- packages/replay/src/integration.ts | 3 +-- packages/utils/src/index.ts | 1 + packages/{replay/src/util => utils/src}/isBrowser.ts | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-) rename packages/{replay/src/util => utils/src}/isBrowser.ts (92%) diff --git a/packages/replay/jest.setup.ts b/packages/replay/jest.setup.ts index b44298a751e1..2c0ec715b7f6 100644 --- a/packages/replay/jest.setup.ts +++ b/packages/replay/jest.setup.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { getCurrentHub } from '@sentry/core'; import type { ReplayRecordingData, Transport } from '@sentry/types'; +import * as SentryUtils from '@sentry/utils'; import { TextEncoder } from 'util'; import type { ReplayContainer, Session } from './src/types'; @@ -10,11 +11,7 @@ import type { ReplayContainer, Session } from './src/types'; type MockTransport = jest.MockedFunction; -jest.mock('./src/util/isBrowser', () => { - return { - isBrowser: () => true, - }; -}); +jest.spyOn(SentryUtils, 'isBrowser').mockImplementation(() => true); type EnvelopeHeader = { event_id: string; diff --git a/packages/replay/src/integration.ts b/packages/replay/src/integration.ts index 725a61681d1c..f0b07582ce61 100644 --- a/packages/replay/src/integration.ts +++ b/packages/replay/src/integration.ts @@ -1,6 +1,6 @@ import { getCurrentHub } from '@sentry/core'; import type { BrowserClientReplayOptions, Integration } from '@sentry/types'; -import { dropUndefinedKeys } from '@sentry/utils'; +import { dropUndefinedKeys, isBrowser } from '@sentry/utils'; import { DEFAULT_FLUSH_MAX_DELAY, @@ -12,7 +12,6 @@ import { import { ReplayContainer } from './replay'; import type { RecordingOptions, ReplayConfiguration, ReplayPluginOptions, SendBufferedReplayOptions } from './types'; import { getPrivacyOptions } from './util/getPrivacyOptions'; -import { isBrowser } from './util/isBrowser'; import { maskAttribute } from './util/maskAttribute'; const MEDIA_SELECTORS = diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 5bb54c52b9cd..454d441080ec 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -5,6 +5,7 @@ export * from './error'; export * from './worldwide'; export * from './instrument'; export * from './is'; +export * from './isBrowser'; export * from './logger'; export * from './memo'; export * from './misc'; diff --git a/packages/replay/src/util/isBrowser.ts b/packages/utils/src/isBrowser.ts similarity index 92% rename from packages/replay/src/util/isBrowser.ts rename to packages/utils/src/isBrowser.ts index 6a64317ba3fa..aa0a4bfa11db 100644 --- a/packages/replay/src/util/isBrowser.ts +++ b/packages/utils/src/isBrowser.ts @@ -1,4 +1,4 @@ -import { isNodeEnv } from '@sentry/utils'; +import { isNodeEnv } from './node'; /** * Returns true if we are in the browser. From 16594de6f7ad7c1cf0111d65383966ce11b90c31 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 20 Oct 2023 10:15:24 +0200 Subject: [PATCH 23/38] fix(node-experimental): Make node-fetch support optional (#9321) Installing this on Node <18 produces install time errors, as the package is not compatible, e.g.: ``` npm WARN EBADENGINE Unsupported engine { npm WARN EBADENGINE package: 'opentelemetry-instrumentation-fetch-node@1.1.0', npm WARN EBADENGINE required: { node: '>18.0.0' }, npm WARN EBADENGINE current: { node: 'v16.18.0', npm: '8.19.2' } npm WARN EBADENGINE } ``` We already try-catch the `setupInstrumentation` hook anyhow, so no need to check again here - if this fails we'll just skip running this integration. --- packages/node-experimental/package.json | 4 +++- .../src/integrations/node-fetch.ts | 23 +++++++++++-------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/node-experimental/package.json b/packages/node-experimental/package.json index 39f226981173..a23df66b8104 100644 --- a/packages/node-experimental/package.json +++ b/packages/node-experimental/package.json @@ -45,7 +45,9 @@ "@sentry/node": "7.74.1", "@sentry/opentelemetry": "7.74.1", "@sentry/types": "7.74.1", - "@sentry/utils": "7.74.1", + "@sentry/utils": "7.74.1" + }, + "optionalDependencies": { "opentelemetry-instrumentation-fetch-node": "1.1.0" }, "scripts": { diff --git a/packages/node-experimental/src/integrations/node-fetch.ts b/packages/node-experimental/src/integrations/node-fetch.ts index 281c6f6d6784..bc1f776d950a 100644 --- a/packages/node-experimental/src/integrations/node-fetch.ts +++ b/packages/node-experimental/src/integrations/node-fetch.ts @@ -4,7 +4,6 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; import { hasTracingEnabled } from '@sentry/core'; import { _INTERNAL, getCurrentHub, getSpanKind } from '@sentry/opentelemetry'; import type { Integration } from '@sentry/types'; -import { FetchInstrumentation } from 'opentelemetry-instrumentation-fetch-node'; import type { NodeExperimentalClient } from '../types'; import { addOriginToSpan } from '../utils/addOriginToSpan'; @@ -66,14 +65,20 @@ export class NodeFetch extends NodePerformanceIntegration impl /** @inheritDoc */ public setupInstrumentation(): void | Instrumentation[] { - return [ - new FetchInstrumentation({ - onRequest: ({ span }: { span: Span }) => { - this._updateSpan(span); - this._addRequestBreadcrumb(span); - }, - }), - ]; + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { FetchInstrumentation } = require('opentelemetry-instrumentation-fetch-node'); + return [ + new FetchInstrumentation({ + onRequest: ({ span }: { span: Span }) => { + this._updateSpan(span); + this._addRequestBreadcrumb(span); + }, + }), + ]; + } catch (error) { + // Could not load instrumentation + } } /** From 046f868d7fb540ec533f798851751ee7fecd0cc2 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 20 Oct 2023 12:01:32 +0200 Subject: [PATCH 24/38] meta(deno): Use https git protocol in deno release target (#9322) --- .craft.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.craft.yml b/.craft.yml index ac8236c36c1c..a7c903b0be78 100644 --- a/.craft.yml +++ b/.craft.yml @@ -73,7 +73,7 @@ targets: - name: commit-on-git-repository # This will publish on the Deno registry archive: /^sentry-deno-\d.*\.tgz$/ - repositoryUrl: git@github.com:getsentry/sentry-deno.git + repositoryUrl: https://github.com/getsentry/sentry-deno.git stripComponents: 1 branch: main createTag: true From 54babf0634e5538de61c96e678acb25397a6b693 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 20 Oct 2023 06:15:16 -0400 Subject: [PATCH 25/38] fix(types): Remove typo with CheckInEnvelope (#9303) --- packages/core/src/checkin.ts | 8 ++++---- packages/types/src/envelope.ts | 4 ++-- packages/types/src/index.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/src/checkin.ts b/packages/core/src/checkin.ts index e7dd6c906f4c..8ffc0d7a60d6 100644 --- a/packages/core/src/checkin.ts +++ b/packages/core/src/checkin.ts @@ -1,5 +1,5 @@ import type { - CheckInEvelope, + CheckInEnvelope, CheckInItem, DsnComponents, DynamicSamplingContext, @@ -17,8 +17,8 @@ export function createCheckInEnvelope( metadata?: SdkMetadata, tunnel?: string, dsn?: DsnComponents, -): CheckInEvelope { - const headers: CheckInEvelope[0] = { +): CheckInEnvelope { + const headers: CheckInEnvelope[0] = { sent_at: new Date().toISOString(), }; @@ -38,7 +38,7 @@ export function createCheckInEnvelope( } const item = createCheckInEnvelopeItem(checkIn); - return createEnvelope(headers, [item]); + return createEnvelope(headers, [item]); } function createCheckInEnvelopeItem(checkIn: SerializedCheckIn): CheckInItem { diff --git a/packages/types/src/envelope.ts b/packages/types/src/envelope.ts index a039874a2d54..051de2a07960 100644 --- a/packages/types/src/envelope.ts +++ b/packages/types/src/envelope.ts @@ -99,8 +99,8 @@ export type EventEnvelope = BaseEnvelope; export type ClientReportEnvelope = BaseEnvelope; export type ReplayEnvelope = [ReplayEnvelopeHeaders, [ReplayEventItem, ReplayRecordingItem]]; -export type CheckInEvelope = BaseEnvelope; +export type CheckInEnvelope = BaseEnvelope; export type StatsdEnvelope = BaseEnvelope; -export type Envelope = EventEnvelope | SessionEnvelope | ClientReportEnvelope | ReplayEnvelope | CheckInEvelope; +export type Envelope = EventEnvelope | SessionEnvelope | ClientReportEnvelope | ReplayEnvelope | CheckInEnvelope; export type EnvelopeItem = Envelope[1][number]; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 8a93681aa938..26a2a4f1328b 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -40,7 +40,7 @@ export type { SessionItem, UserFeedbackItem, CheckInItem, - CheckInEvelope, + CheckInEnvelope, } from './envelope'; export type { ExtendedError } from './error'; export type { Event, EventHint, EventType, ErrorEvent, TransactionEvent } from './event'; From 38730e8d1fd0f963265c6e1c35e21283be4d2d52 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 20 Oct 2023 13:21:14 +0200 Subject: [PATCH 26/38] meta(craft): Add id to Deno target (#9311) --- .craft.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.craft.yml b/.craft.yml index a7c903b0be78..46f270120a91 100644 --- a/.craft.yml +++ b/.craft.yml @@ -72,6 +72,7 @@ targets: includeNames: /^sentry-deno-\d.*\.tgz$/ - name: commit-on-git-repository # This will publish on the Deno registry + id: getsentry/deno archive: /^sentry-deno-\d.*\.tgz$/ repositoryUrl: https://github.com/getsentry/sentry-deno.git stripComponents: 1 From 42a60b614f6310ad4fd172abc23b119bf348293f Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 20 Oct 2023 15:12:07 +0200 Subject: [PATCH 27/38] test(e2e): Add test for local variables (#9278) --- .../node-express-app/event-proxy-server.ts | 253 ++++++++++++++++++ .../node-express-app/package.json | 3 +- .../node-express-app/playwright.config.ts | 25 +- .../node-express-app/src/app.ts | 30 +++ .../node-express-app/start-event-proxy.ts | 6 + .../node-express-app/tests/server.test.ts | 57 +++- 6 files changed, 368 insertions(+), 6 deletions(-) create mode 100644 packages/e2e-tests/test-applications/node-express-app/event-proxy-server.ts create mode 100644 packages/e2e-tests/test-applications/node-express-app/start-event-proxy.ts diff --git a/packages/e2e-tests/test-applications/node-express-app/event-proxy-server.ts b/packages/e2e-tests/test-applications/node-express-app/event-proxy-server.ts new file mode 100644 index 000000000000..67cf80b4dabf --- /dev/null +++ b/packages/e2e-tests/test-applications/node-express-app/event-proxy-server.ts @@ -0,0 +1,253 @@ +import type { Envelope, EnvelopeItem, Event } from '@sentry/types'; +import { parseEnvelope } from '@sentry/utils'; +import * as fs from 'fs'; +import * as http from 'http'; +import * as https from 'https'; +import type { AddressInfo } from 'net'; +import * as os from 'os'; +import * as path from 'path'; +import * as util from 'util'; +import * as zlib from 'zlib'; + +const readFile = util.promisify(fs.readFile); +const writeFile = util.promisify(fs.writeFile); + +interface EventProxyServerOptions { + /** Port to start the event proxy server at. */ + port: number; + /** The name for the proxy server used for referencing it with listener functions */ + proxyServerName: string; +} + +interface SentryRequestCallbackData { + envelope: Envelope; + rawProxyRequestBody: string; + rawSentryResponseBody: string; + sentryResponseStatusCode?: number; +} + +/** + * Starts an event proxy server that will proxy events to sentry when the `tunnel` option is used. Point the `tunnel` + * option to this server (like this `tunnel: http://localhost:${port option}/`). + */ +export async function startEventProxyServer(options: EventProxyServerOptions): Promise { + const eventCallbackListeners: Set<(data: string) => void> = new Set(); + + const proxyServer = http.createServer((proxyRequest, proxyResponse) => { + const proxyRequestChunks: Uint8Array[] = []; + + proxyRequest.addListener('data', (chunk: Buffer) => { + proxyRequestChunks.push(chunk); + }); + + proxyRequest.addListener('error', err => { + throw err; + }); + + proxyRequest.addListener('end', () => { + const proxyRequestBody = + proxyRequest.headers['content-encoding'] === 'gzip' + ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString() + : Buffer.concat(proxyRequestChunks).toString(); + + let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]); + + if (!envelopeHeader.dsn) { + throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.'); + } + + const { origin, pathname, host } = new URL(envelopeHeader.dsn); + + const projectId = pathname.substring(1); + const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`; + + proxyRequest.headers.host = host; + + const sentryResponseChunks: Uint8Array[] = []; + + const sentryRequest = https.request( + sentryIngestUrl, + { headers: proxyRequest.headers, method: proxyRequest.method }, + sentryResponse => { + sentryResponse.addListener('data', (chunk: Buffer) => { + proxyResponse.write(chunk, 'binary'); + sentryResponseChunks.push(chunk); + }); + + sentryResponse.addListener('end', () => { + eventCallbackListeners.forEach(listener => { + const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString(); + + const data: SentryRequestCallbackData = { + envelope: parseEnvelope(proxyRequestBody, new TextEncoder(), new TextDecoder()), + rawProxyRequestBody: proxyRequestBody, + rawSentryResponseBody, + sentryResponseStatusCode: sentryResponse.statusCode, + }; + + listener(Buffer.from(JSON.stringify(data)).toString('base64')); + }); + proxyResponse.end(); + }); + + sentryResponse.addListener('error', err => { + throw err; + }); + + proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers); + }, + ); + + sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary'); + sentryRequest.end(); + }); + }); + + const proxyServerStartupPromise = new Promise(resolve => { + proxyServer.listen(options.port, () => { + resolve(); + }); + }); + + const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => { + eventCallbackResponse.statusCode = 200; + eventCallbackResponse.setHeader('connection', 'keep-alive'); + + const callbackListener = (data: string): void => { + eventCallbackResponse.write(data.concat('\n'), 'utf8'); + }; + + eventCallbackListeners.add(callbackListener); + + eventCallbackRequest.on('close', () => { + eventCallbackListeners.delete(callbackListener); + }); + + eventCallbackRequest.on('error', () => { + eventCallbackListeners.delete(callbackListener); + }); + }); + + const eventCallbackServerStartupPromise = new Promise(resolve => { + eventCallbackServer.listen(0, () => { + const port = String((eventCallbackServer.address() as AddressInfo).port); + void registerCallbackServerPort(options.proxyServerName, port).then(resolve); + }); + }); + + await eventCallbackServerStartupPromise; + await proxyServerStartupPromise; + return; +} + +export async function waitForRequest( + proxyServerName: string, + callback: (eventData: SentryRequestCallbackData) => Promise | boolean, +): Promise { + const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName); + + return new Promise((resolve, reject) => { + const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => { + let eventContents = ''; + + response.on('error', err => { + reject(err); + }); + + response.on('data', (chunk: Buffer) => { + const chunkString = chunk.toString('utf8'); + chunkString.split('').forEach(char => { + if (char === '\n') { + const eventCallbackData: SentryRequestCallbackData = JSON.parse( + Buffer.from(eventContents, 'base64').toString('utf8'), + ); + const callbackResult = callback(eventCallbackData); + if (typeof callbackResult !== 'boolean') { + callbackResult.then( + match => { + if (match) { + response.destroy(); + resolve(eventCallbackData); + } + }, + err => { + throw err; + }, + ); + } else if (callbackResult) { + response.destroy(); + resolve(eventCallbackData); + } + eventContents = ''; + } else { + eventContents = eventContents.concat(char); + } + }); + }); + }); + + request.end(); + }); +} + +export function waitForEnvelopeItem( + proxyServerName: string, + callback: (envelopeItem: EnvelopeItem) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForRequest(proxyServerName, async eventData => { + const envelopeItems = eventData.envelope[1]; + for (const envelopeItem of envelopeItems) { + if (await callback(envelopeItem)) { + resolve(envelopeItem); + return true; + } + } + return false; + }).catch(reject); + }); +} + +export function waitForError( + proxyServerName: string, + callback: (transactionEvent: Event) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForEnvelopeItem(proxyServerName, async envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) { + resolve(envelopeItemBody as Event); + return true; + } + return false; + }).catch(reject); + }); +} + +export function waitForTransaction( + proxyServerName: string, + callback: (transactionEvent: Event) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForEnvelopeItem(proxyServerName, async envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) { + resolve(envelopeItemBody as Event); + return true; + } + return false; + }).catch(reject); + }); +} + +const TEMP_FILE_PREFIX = 'event-proxy-server-'; + +async function registerCallbackServerPort(serverName: string, port: string): Promise { + const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); + await writeFile(tmpFilePath, port, { encoding: 'utf8' }); +} + +function retrieveCallbackServerPort(serverName: string): Promise { + const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); + return readFile(tmpFilePath, 'utf8'); +} diff --git a/packages/e2e-tests/test-applications/node-express-app/package.json b/packages/e2e-tests/test-applications/node-express-app/package.json index 02dcb35da7fe..2d44043b94e7 100644 --- a/packages/e2e-tests/test-applications/node-express-app/package.json +++ b/packages/e2e-tests/test-applications/node-express-app/package.json @@ -21,7 +21,8 @@ "typescript": "4.9.5" }, "devDependencies": { - "@playwright/test": "^1.27.1" + "@playwright/test": "^1.27.1", + "ts-node": "10.9.1" }, "volta": { "extends": "../../package.json" diff --git a/packages/e2e-tests/test-applications/node-express-app/playwright.config.ts b/packages/e2e-tests/test-applications/node-express-app/playwright.config.ts index 879d7b3d2093..d3fbb6971415 100644 --- a/packages/e2e-tests/test-applications/node-express-app/playwright.config.ts +++ b/packages/e2e-tests/test-applications/node-express-app/playwright.config.ts @@ -1,6 +1,14 @@ import type { PlaywrightTestConfig } from '@playwright/test'; import { devices } from '@playwright/test'; +// Fix urls not resolving to localhost on Node v17+ +// See: https://github.com/axios/axios/issues/3821#issuecomment-1413727575 +import { setDefaultResultOrder } from 'dns'; +setDefaultResultOrder('ipv4first'); + +const eventProxyPort = 3031; +const expressPort = 3030; + /** * See https://playwright.dev/docs/test-configuration. */ @@ -28,6 +36,9 @@ const config: PlaywrightTestConfig = { /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: `http://localhost:${expressPort}`, + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', }, @@ -56,10 +67,16 @@ const config: PlaywrightTestConfig = { ], /* Run your local dev server before starting the tests */ - webServer: { - command: 'pnpm start', - port: 3030, - }, + webServer: [ + { + command: 'pnpm ts-node-script start-event-proxy.ts', + port: eventProxyPort, + }, + { + command: 'pnpm start', + port: expressPort, + }, + ], }; export default config; diff --git a/packages/e2e-tests/test-applications/node-express-app/src/app.ts b/packages/e2e-tests/test-applications/node-express-app/src/app.ts index 16aa82545b85..9316c9a2a912 100644 --- a/packages/e2e-tests/test-applications/node-express-app/src/app.ts +++ b/packages/e2e-tests/test-applications/node-express-app/src/app.ts @@ -12,8 +12,10 @@ declare global { Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.E2E_TEST_DSN, + includeLocalVariables: true, integrations: [new Integrations.HttpClient()], debug: true, + tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, }); @@ -55,6 +57,34 @@ app.get('/test-error', async function (req, res) { res.send({ exceptionId }); }); +app.get('/test-local-variables-uncaught', function (req, res) { + const randomVariableToRecord = Math.random(); + throw new Error(`Uncaught Local Variable Error - ${JSON.stringify({ randomVariableToRecord })}`); +}); + +app.get('/test-local-variables-caught', function (req, res) { + const randomVariableToRecord = Math.random(); + + let exceptionId: string; + try { + throw new Error('Local Variable Error'); + } catch (e) { + exceptionId = Sentry.captureException(e); + } + + res.send({ exceptionId, randomVariableToRecord }); +}); + +app.use(Sentry.Handlers.errorHandler()); + +// @ts-ignore +app.use(function onError(err, req, res, next) { + // The error id is attached to `res.sentry` to be returned + // and optionally displayed to the user for support. + res.statusCode = 500; + res.end(res.sentry + '\n'); +}); + app.listen(port, () => { console.log(`Example app listening on port ${port}`); }); diff --git a/packages/e2e-tests/test-applications/node-express-app/start-event-proxy.ts b/packages/e2e-tests/test-applications/node-express-app/start-event-proxy.ts new file mode 100644 index 000000000000..376afc851351 --- /dev/null +++ b/packages/e2e-tests/test-applications/node-express-app/start-event-proxy.ts @@ -0,0 +1,6 @@ +import { startEventProxyServer } from './event-proxy-server'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'node-express-app', +}); diff --git a/packages/e2e-tests/test-applications/node-express-app/tests/server.test.ts b/packages/e2e-tests/test-applications/node-express-app/tests/server.test.ts index 654827cb8e03..4429c2d46edd 100644 --- a/packages/e2e-tests/test-applications/node-express-app/tests/server.test.ts +++ b/packages/e2e-tests/test-applications/node-express-app/tests/server.test.ts @@ -1,5 +1,6 @@ import { test, expect } from '@playwright/test'; -import axios, { AxiosError } from 'axios'; +import axios, { AxiosError, AxiosResponse } from 'axios'; +import { waitForError } from '../event-proxy-server'; const authToken = process.env.E2E_TEST_AUTH_TOKEN; const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; @@ -75,3 +76,57 @@ test('Sends transactions to Sentry', async ({ baseURL }) => { }), ); }); + +test('Should record caught exceptions with local variable', async ({ baseURL }) => { + const { data } = await axios.get(`${baseURL}/test-local-variables-caught`); + const { exceptionId } = data; + + const url = `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionId}/json/`; + + console.log(`Polling for error eventId: ${exceptionId}`); + + let response: AxiosResponse; + + await expect + .poll( + async () => { + try { + response = await axios.get(url, { headers: { Authorization: `Bearer ${authToken}` } }); + + return response.status; + } catch (e) { + if (e instanceof AxiosError && e.response) { + if (e.response.status !== 404) { + throw e; + } else { + return e.response.status; + } + } else { + throw e; + } + } + }, + { timeout: EVENT_POLLING_TIMEOUT }, + ) + .toBe(200); + + const frames = response!.data.exception.values[0].stacktrace.frames; + + expect(frames[frames.length - 1].vars?.randomVariableToRecord).toBeDefined(); +}); + +test('Should record uncaught exceptions with local variable', async ({ baseURL }) => { + const errorEventPromise = waitForError('node-express-app', errorEvent => { + return !!errorEvent?.exception?.values?.[0]?.value?.includes('Uncaught Local Variable Error'); + }); + + await axios.get(`${baseURL}/test-local-variables-uncaught`).catch(() => { + // noop + }); + + const routehandlerError = await errorEventPromise; + + const frames = routehandlerError!.exception!.values![0]!.stacktrace!.frames!; + + expect(frames[frames.length - 1].vars?.randomVariableToRecord).toBeDefined(); +}); From c5c12a98dd7d8c8446a2a27ed586dbf03f4d0c91 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 23 Oct 2023 10:52:25 +0200 Subject: [PATCH 28/38] test(e2e): use `127.0.0.1` instead of `localhost` for E2E `.npmrc` (#9330) Locally E2E tests keep hanging due to this, for whatever reason. Probably some DSN issues, but no idea why... Changing this to `127.0.0.1` fixes the problem :O --- packages/e2e-tests/README.md | 6 +++--- packages/e2e-tests/test-applications/create-next-app/.npmrc | 4 ++-- .../e2e-tests/test-applications/create-react-app/.npmrc | 4 ++-- .../e2e-tests/test-applications/create-remix-app-v2/.npmrc | 4 ++-- .../e2e-tests/test-applications/create-remix-app/.npmrc | 4 ++-- .../e2e-tests/test-applications/debug-id-sourcemaps/.npmrc | 4 ++-- packages/e2e-tests/test-applications/generic-ts3.8/.npmrc | 4 ++-- packages/e2e-tests/test-applications/nextjs-app-dir/.npmrc | 4 ++-- .../test-applications/node-experimental-fastify-app/.npmrc | 4 ++-- .../e2e-tests/test-applications/node-express-app/.npmrc | 4 ++-- .../test-applications/react-create-hash-router/.npmrc | 4 ++-- .../test-applications/react-router-6-use-routes/.npmrc | 4 ++-- .../standard-frontend-react-tracing-import/.npmrc | 4 ++-- .../test-applications/standard-frontend-react/.npmrc | 4 ++-- packages/e2e-tests/test-applications/sveltekit/.npmrc | 4 ++-- packages/e2e-tests/test-registry.npmrc | 6 +++--- 16 files changed, 34 insertions(+), 34 deletions(-) diff --git a/packages/e2e-tests/README.md b/packages/e2e-tests/README.md index 1917c3710e54..541257eb2410 100644 --- a/packages/e2e-tests/README.md +++ b/packages/e2e-tests/README.md @@ -49,8 +49,8 @@ mkdir test-applications/my-new-test-application # Name of the new folder doesn't # Create an npm configuration file that uses the fake test registry cat > test-applications/my-new-test-application/.npmrc << EOF -@sentry:registry=http://localhost:4873 -@sentry-internal:registry=http://localhost:4873 +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 EOF ``` @@ -60,7 +60,7 @@ Add the new test app to `test-application` matrix in `.github/workflows/build.ym want to run a canary test, add it to the `canary.yml` workflow. **An important thing to note:** In the context of the build/test commands the fake test registry is available at -`http://localhost:4873`. It hosts all of our packages as if they were to be published with the state of the current +`http://127.0.0.1:4873`. It hosts all of our packages as if they were to be published with the state of the current branch. This means we can install the packages from this registry via the `.npmrc` configuration as seen above. If you add Sentry dependencies to your test application, you should set the dependency versions set to `latest || *` in order for it to work with both regular and prerelease versions: diff --git a/packages/e2e-tests/test-applications/create-next-app/.npmrc b/packages/e2e-tests/test-applications/create-next-app/.npmrc index c6b3ef9b3eaa..070f80f05092 100644 --- a/packages/e2e-tests/test-applications/create-next-app/.npmrc +++ b/packages/e2e-tests/test-applications/create-next-app/.npmrc @@ -1,2 +1,2 @@ -@sentry:registry=http://localhost:4873 -@sentry-internal:registry=http://localhost:4873 +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/packages/e2e-tests/test-applications/create-react-app/.npmrc b/packages/e2e-tests/test-applications/create-react-app/.npmrc index c6b3ef9b3eaa..070f80f05092 100644 --- a/packages/e2e-tests/test-applications/create-react-app/.npmrc +++ b/packages/e2e-tests/test-applications/create-react-app/.npmrc @@ -1,2 +1,2 @@ -@sentry:registry=http://localhost:4873 -@sentry-internal:registry=http://localhost:4873 +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/packages/e2e-tests/test-applications/create-remix-app-v2/.npmrc b/packages/e2e-tests/test-applications/create-remix-app-v2/.npmrc index c6b3ef9b3eaa..070f80f05092 100644 --- a/packages/e2e-tests/test-applications/create-remix-app-v2/.npmrc +++ b/packages/e2e-tests/test-applications/create-remix-app-v2/.npmrc @@ -1,2 +1,2 @@ -@sentry:registry=http://localhost:4873 -@sentry-internal:registry=http://localhost:4873 +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/packages/e2e-tests/test-applications/create-remix-app/.npmrc b/packages/e2e-tests/test-applications/create-remix-app/.npmrc index c6b3ef9b3eaa..070f80f05092 100644 --- a/packages/e2e-tests/test-applications/create-remix-app/.npmrc +++ b/packages/e2e-tests/test-applications/create-remix-app/.npmrc @@ -1,2 +1,2 @@ -@sentry:registry=http://localhost:4873 -@sentry-internal:registry=http://localhost:4873 +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/packages/e2e-tests/test-applications/debug-id-sourcemaps/.npmrc b/packages/e2e-tests/test-applications/debug-id-sourcemaps/.npmrc index c6b3ef9b3eaa..070f80f05092 100644 --- a/packages/e2e-tests/test-applications/debug-id-sourcemaps/.npmrc +++ b/packages/e2e-tests/test-applications/debug-id-sourcemaps/.npmrc @@ -1,2 +1,2 @@ -@sentry:registry=http://localhost:4873 -@sentry-internal:registry=http://localhost:4873 +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/packages/e2e-tests/test-applications/generic-ts3.8/.npmrc b/packages/e2e-tests/test-applications/generic-ts3.8/.npmrc index c6b3ef9b3eaa..070f80f05092 100644 --- a/packages/e2e-tests/test-applications/generic-ts3.8/.npmrc +++ b/packages/e2e-tests/test-applications/generic-ts3.8/.npmrc @@ -1,2 +1,2 @@ -@sentry:registry=http://localhost:4873 -@sentry-internal:registry=http://localhost:4873 +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/.npmrc b/packages/e2e-tests/test-applications/nextjs-app-dir/.npmrc index c6b3ef9b3eaa..070f80f05092 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/.npmrc +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/.npmrc @@ -1,2 +1,2 @@ -@sentry:registry=http://localhost:4873 -@sentry-internal:registry=http://localhost:4873 +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/packages/e2e-tests/test-applications/node-experimental-fastify-app/.npmrc b/packages/e2e-tests/test-applications/node-experimental-fastify-app/.npmrc index c6b3ef9b3eaa..070f80f05092 100644 --- a/packages/e2e-tests/test-applications/node-experimental-fastify-app/.npmrc +++ b/packages/e2e-tests/test-applications/node-experimental-fastify-app/.npmrc @@ -1,2 +1,2 @@ -@sentry:registry=http://localhost:4873 -@sentry-internal:registry=http://localhost:4873 +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/packages/e2e-tests/test-applications/node-express-app/.npmrc b/packages/e2e-tests/test-applications/node-express-app/.npmrc index c6b3ef9b3eaa..070f80f05092 100644 --- a/packages/e2e-tests/test-applications/node-express-app/.npmrc +++ b/packages/e2e-tests/test-applications/node-express-app/.npmrc @@ -1,2 +1,2 @@ -@sentry:registry=http://localhost:4873 -@sentry-internal:registry=http://localhost:4873 +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/packages/e2e-tests/test-applications/react-create-hash-router/.npmrc b/packages/e2e-tests/test-applications/react-create-hash-router/.npmrc index c6b3ef9b3eaa..070f80f05092 100644 --- a/packages/e2e-tests/test-applications/react-create-hash-router/.npmrc +++ b/packages/e2e-tests/test-applications/react-create-hash-router/.npmrc @@ -1,2 +1,2 @@ -@sentry:registry=http://localhost:4873 -@sentry-internal:registry=http://localhost:4873 +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/packages/e2e-tests/test-applications/react-router-6-use-routes/.npmrc b/packages/e2e-tests/test-applications/react-router-6-use-routes/.npmrc index c6b3ef9b3eaa..070f80f05092 100644 --- a/packages/e2e-tests/test-applications/react-router-6-use-routes/.npmrc +++ b/packages/e2e-tests/test-applications/react-router-6-use-routes/.npmrc @@ -1,2 +1,2 @@ -@sentry:registry=http://localhost:4873 -@sentry-internal:registry=http://localhost:4873 +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/.npmrc b/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/.npmrc index c6b3ef9b3eaa..070f80f05092 100644 --- a/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/.npmrc +++ b/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/.npmrc @@ -1,2 +1,2 @@ -@sentry:registry=http://localhost:4873 -@sentry-internal:registry=http://localhost:4873 +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/packages/e2e-tests/test-applications/standard-frontend-react/.npmrc b/packages/e2e-tests/test-applications/standard-frontend-react/.npmrc index c6b3ef9b3eaa..070f80f05092 100644 --- a/packages/e2e-tests/test-applications/standard-frontend-react/.npmrc +++ b/packages/e2e-tests/test-applications/standard-frontend-react/.npmrc @@ -1,2 +1,2 @@ -@sentry:registry=http://localhost:4873 -@sentry-internal:registry=http://localhost:4873 +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/packages/e2e-tests/test-applications/sveltekit/.npmrc b/packages/e2e-tests/test-applications/sveltekit/.npmrc index c6b3ef9b3eaa..070f80f05092 100644 --- a/packages/e2e-tests/test-applications/sveltekit/.npmrc +++ b/packages/e2e-tests/test-applications/sveltekit/.npmrc @@ -1,2 +1,2 @@ -@sentry:registry=http://localhost:4873 -@sentry-internal:registry=http://localhost:4873 +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/packages/e2e-tests/test-registry.npmrc b/packages/e2e-tests/test-registry.npmrc index fd8ba6605a28..97b9627a1642 100644 --- a/packages/e2e-tests/test-registry.npmrc +++ b/packages/e2e-tests/test-registry.npmrc @@ -1,6 +1,6 @@ -@sentry:registry=http://localhost:4873 -@sentry-internal:registry=http://localhost:4873 -//localhost:4873/:_authToken=some-token +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 +//127.0.0.1:4873/:_authToken=some-token # Do not notify about npm updates update-notifier=false From 9218eaa2e9a8ee4994fa19d7e6cfcdab556e2383 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 23 Oct 2023 11:02:54 +0200 Subject: [PATCH 29/38] fix(nextjs): Restore `autoInstrumentMiddleware` functionality (#9323) --- packages/nextjs/src/config/webpack.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 2594623fb5bb..01cbca20806a 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -243,18 +243,20 @@ export function constructWebpackConfigFunction( }); // Wrap middleware - newConfig.module.rules.unshift({ - test: isMiddlewareResource, - use: [ - { - loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'), - options: { - ...staticWrappingLoaderOptions, - wrappingTargetKind: 'middleware', + if (userSentryOptions.autoInstrumentMiddleware ?? true) { + newConfig.module.rules.unshift({ + test: isMiddlewareResource, + use: [ + { + loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'), + options: { + ...staticWrappingLoaderOptions, + wrappingTargetKind: 'middleware', + }, }, - }, - ], - }); + ], + }); + } } if (isServer && userSentryOptions.autoInstrumentAppDirectory !== false) { From a6c44afaf3d398c2dea58887e9e756e56e5f05cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=BDubom=C3=ADrIgonda?= <9303146+LubomirIgonda1@users.noreply.github.com> Date: Mon, 23 Oct 2023 12:33:15 +0200 Subject: [PATCH 30/38] fix(tracing-internal): Remove query params from urls with a trailing slash (#9328) fix use case when original req url ends with trailing slash and contains query params. Example: `api/v1/users/123/posts/?param=1` --- .../multiple-routers/complex-router/test.ts | 52 +++++++++++++++++++ .../src/node/integrations/express.ts | 2 +- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/packages/node-integration-tests/suites/express/multiple-routers/complex-router/test.ts b/packages/node-integration-tests/suites/express/multiple-routers/complex-router/test.ts index b8079bfdc0ac..05de34cc8159 100644 --- a/packages/node-integration-tests/suites/express/multiple-routers/complex-router/test.ts +++ b/packages/node-integration-tests/suites/express/multiple-routers/complex-router/test.ts @@ -25,3 +25,55 @@ test('should construct correct url with multiple parameterized routers, when par }); } }); + +test('should construct correct url with multiple parameterized routers, when param is also contain in middle layer route and express used multiple middlewares with route and original url has query params', async () => { + const env = await TestEnv.init(__dirname, `${__dirname}/server.ts`); + const event = await env.getEnvelopeRequest({ + url: env.url.replace('test', 'api/api/v1/sub-router/users/123/posts/456?param=1'), + envelopeType: 'transaction', + }); + // parse node.js major version + const [major] = process.versions.node.split('.').map(Number); + // Split test result base on major node version because regex d flag is support from node 16+ + if (major >= 16) { + assertSentryEvent(event[2] as any, { + transaction: 'GET /api/api/v1/sub-router/users/:userId/posts/:postId', + transaction_info: { + source: 'route', + }, + }); + } else { + assertSentryEvent(event[2] as any, { + transaction: 'GET /api/api/v1/sub-router/users/123/posts/:postId', + transaction_info: { + source: 'route', + }, + }); + } +}); + +test('should construct correct url with multiple parameterized routers, when param is also contain in middle layer route and express used multiple middlewares with route and original url ends with trailing slash and has query params', async () => { + const env = await TestEnv.init(__dirname, `${__dirname}/server.ts`); + const event = await env.getEnvelopeRequest({ + url: env.url.replace('test', 'api/api/v1/sub-router/users/123/posts/456/?param=1'), + envelopeType: 'transaction', + }); + // parse node.js major version + const [major] = process.versions.node.split('.').map(Number); + // Split test result base on major node version because regex d flag is support from node 16+ + if (major >= 16) { + assertSentryEvent(event[2] as any, { + transaction: 'GET /api/api/v1/sub-router/users/:userId/posts/:postId', + transaction_info: { + source: 'route', + }, + }); + } else { + assertSentryEvent(event[2] as any, { + transaction: 'GET /api/api/v1/sub-router/users/123/posts/:postId', + transaction_info: { + source: 'route', + }, + }); + } +}); diff --git a/packages/tracing-internal/src/node/integrations/express.ts b/packages/tracing-internal/src/node/integrations/express.ts index 4327f11c5ea7..e46096d9ed84 100644 --- a/packages/tracing-internal/src/node/integrations/express.ts +++ b/packages/tracing-internal/src/node/integrations/express.ts @@ -358,7 +358,7 @@ function instrumentRouter(appOrRouter: ExpressRouter): void { // Now we check if we are in the "last" part of the route. We determine this by comparing the // number of URL segments from the original URL to that of our reconstructed parameterized URL. // If we've reached our final destination, we update the transaction name. - const urlLength = getNumberOfUrlSegments(req.originalUrl || '') + numExtraSegments; + const urlLength = getNumberOfUrlSegments(stripUrlQueryAndFragment(req.originalUrl || '')) + numExtraSegments; const routeLength = getNumberOfUrlSegments(req._reconstructedRoute); if (urlLength === routeLength) { From 07b4c76fb84b0cd2370ee3b0520f818bf40d7707 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 23 Oct 2023 13:16:41 +0100 Subject: [PATCH 31/38] doc(astro): Update Readme (#9333) Update the Astro readme with up-to-date installation instructions and links to our docs. --- packages/astro/README.md | 102 ++++++++------------------------------- 1 file changed, 21 insertions(+), 81 deletions(-) diff --git a/packages/astro/README.md b/packages/astro/README.md index a2738298a67c..4470d854c717 100644 --- a/packages/astro/README.md +++ b/packages/astro/README.md @@ -10,12 +10,9 @@ [![npm dm](https://img.shields.io/npm/dm/@sentry/astro.svg)](https://www.npmjs.com/package/@sentry/astro) [![npm dt](https://img.shields.io/npm/dt/@sentry/astro.svg)](https://www.npmjs.com/package/@sentry/astro) - ## Experimental Note @@ -28,98 +25,41 @@ This package is a wrapper around `@sentry/node` for the server and `@sentry/brow ## Installation and Setup -### 1. Registering the Sentry Astro integration: +Install the Sentry Astro SDK with the `astro` CLI: -Add the `sentryAstro` integration to your `astro.config.mjs` file: - -```javascript -import { sentryAstro } from "@sentry/astro/integration"; - -export default defineConfig({ - // Rest of your Astro project config - integrations: [ - sentryAstro({ - dsn: '__DSN__', - }), - ], -}) +```bash +npx astro add @sentry/astro ``` -This is the easiest way to configure Sentry in an Astro project. -You can pass a few additional options to `sentryAstro` but the SDK comes preconfigured in an opinionated way. -If you want to fully customize your SDK setup, you can do so, too: +Complete the setup by adding your DSN and source maps upload configuration: -### 2. [Optional] Uploading Source Maps - -To upload source maps to Sentry, simply add the `project` and `authToken` options to `sentryAstro`: - -```js -// astro.config.mjs -import { sentryAstro } from "@sentry/astro/integration"; +```javascript +import { defineConfig } from "astro/config"; +import sentry from "@sentry/astro"; export default defineConfig({ - // Rest of your Astro project config integrations: [ - sentryAstro({ - dsn: '__DSN__', - project: 'your-project-slug', - authToken: import.meta.env('SENTRY_AUTH_TOKEN'), + sentry({ + dsn: "__DSN__", + sourceMapsUploadOptions: { + project: "your-sentry-project-slug", + authToken: process.env.SENTRY_AUTH_TOKEN, + }, }), ], -}) +}); ``` -You can also define these values as environment variables in e.g. a `.env` file -or in you CI configuration: +Follow [this guide](https://docs.sentry.io/product/accounts/auth-tokens/#organization-auth-tokens) to create an auth token and add it to your environment variables: -```sh -SENTRY_PROJECT="your-project" +```bash SENTRY_AUTH_TOKEN="your-token" ``` -Follow [this guide](https://docs.sentry.io/product/accounts/auth-tokens/#organization-auth-tokens) to create an auth token. - -### 3. [Optional] Advanced Configuration - -To fully customize and configure Sentry in an Astro project, follow step 1 and in addition, -add a `sentry.client.config.(js|ts)` and `sentry.server.config(js|ts)` file to the root directory of your project. -Inside these files, you can call `Sentry.init()` and use the full range of Sentry options. - -Configuring the client SDK: - -```js -// sentry.client.config.ts or sentry.server.config.ts -import * as Sentry from "@sentry/astro"; - -Sentry.init({ - dsn: "__DSN__", - beforeSend(event) { - console.log("Sending event on the client"); - return event; - }, - tracesSampler: () => {/* ... */} -}); -``` - -**Important**: Once you created a sentry config file, the SDK options passed to `sentryAstro` will be ignored for the respective runtime. You can also only define create of the two files. - -#### 3.1 Custom file location +## Configuration -If you want to move the `sentry.*.config` files to another location, -you can specify the file path, relative to the project root, in `sentryAstro`: +Check out our docs for configuring your SDK setup: -```js -// astro.config.mjs -import { sentryAstro } from "@sentry/astro/integration"; - -export default defineConfig({ - // Rest of your Astro project config - integrations: [ - sentryAstro({ - dsn: '__DSN__', - clientInitPath: '.config/sentry.client.init.js', - serverInitPath: '.config/sentry.server.init.js', - }), - ], -}) -``` +* [Getting Started](https://docs.sentry.io/platforms/javascript/guides/astro/) +* [Manual Setup and Configuration](https://docs.sentry.io/platforms/javascript/guides/astro/manual-setup/) +* [Source Maps Upload](https://docs.sentry.io/platforms/javascript/guides/astro/sourcemaps/) From 1245e91206b74bcefddcb2f2f2a87c65981ef68c Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 23 Oct 2023 15:07:24 +0200 Subject: [PATCH 32/38] fix(integrations): Fix transaction integration (#9334) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was not covered well by our tests 😬 oops... Fixes https://github.com/getsentry/sentry-javascript/issues/9332 --- packages/integrations/src/transaction.ts | 2 +- packages/integrations/test/transaction.test.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/integrations/src/transaction.ts b/packages/integrations/src/transaction.ts index ae9f826cba55..bc36857f6538 100644 --- a/packages/integrations/src/transaction.ts +++ b/packages/integrations/src/transaction.ts @@ -25,7 +25,7 @@ export class Transaction implements Integration { /** @inheritDoc */ public processEvent(event: Event): Event { - return this.processEvent(event); + return this.process(event); } /** diff --git a/packages/integrations/test/transaction.test.ts b/packages/integrations/test/transaction.test.ts index 3d1ad895741f..9a87369fb234 100644 --- a/packages/integrations/test/transaction.test.ts +++ b/packages/integrations/test/transaction.test.ts @@ -5,7 +5,7 @@ const transaction: Transaction = new Transaction(); describe('Transaction', () => { describe('extracts info from module/function of the first `in_app` frame', () => { it('using module only', () => { - const event = transaction.process({ + const event = transaction.processEvent({ exception: { values: [ { @@ -31,7 +31,7 @@ describe('Transaction', () => { }); it('using function only', () => { - const event = transaction.process({ + const event = transaction.processEvent({ exception: { values: [ { @@ -57,7 +57,7 @@ describe('Transaction', () => { }); it('using module and function', () => { - const event = transaction.process({ + const event = transaction.processEvent({ exception: { values: [ { @@ -85,7 +85,7 @@ describe('Transaction', () => { }); it('using default', () => { - const event = transaction.process({ + const event = transaction.processEvent({ exception: { values: [ { @@ -109,7 +109,7 @@ describe('Transaction', () => { }); it('no value with no `in_app` frame', () => { - const event = transaction.process({ + const event = transaction.processEvent({ exception: { values: [ { From 8e603b51916426cb448a1e66e0c7156cfde719c1 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 23 Oct 2023 16:16:30 +0100 Subject: [PATCH 33/38] fix(astro): Convert SDK init file import paths to POSIX paths (#9336) Convert init file import paths to POSIX paths to ensure Vite can resolve the path to the file correctly when the import statement to inject the file into the client and browser bundles is added. --- packages/astro/src/integration/snippets.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/astro/src/integration/snippets.ts b/packages/astro/src/integration/snippets.ts index 3b732b55a330..2cf309ea1cb2 100644 --- a/packages/astro/src/integration/snippets.ts +++ b/packages/astro/src/integration/snippets.ts @@ -1,10 +1,12 @@ +import * as path from 'path'; + import type { SentryOptions } from './types'; /** * Creates a snippet that imports a Sentry.init file. */ export function buildSdkInitFileImportSnippet(filePath: string): string { - return `import "${filePath}";`; + return `import "${pathToPosix(filePath)}";`; } /** @@ -69,3 +71,7 @@ const buildClientIntegrations = (options: SentryOptions): string => { return integrations.join(', '); }; + +function pathToPosix(originalPath: string): string { + return originalPath.split(path.sep).join(path.posix.sep); +} From a410b049f108072787173fadc890c473ae64b3d8 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 24 Oct 2023 08:25:24 +0100 Subject: [PATCH 34/38] fix(astro): Add integration default export to types entry point (#9337) Adds the @sentry/astro package's default export (i.e. the `sentryAstro` integration) to the types entry poin --- packages/astro/src/index.types.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/astro/src/index.types.ts b/packages/astro/src/index.types.ts index e8ff7457f597..d1d04f3a4bb8 100644 --- a/packages/astro/src/index.types.ts +++ b/packages/astro/src/index.types.ts @@ -10,6 +10,7 @@ import type { Integration, Options, StackParser } from '@sentry/types'; import type * as clientSdk from './index.client'; import type * as serverSdk from './index.server'; +import sentryAstro from './index.server'; /** Initializes Sentry Astro SDK */ export declare function init(options: Options | clientSdk.BrowserOptions | serverSdk.NodeOptions): void; @@ -23,3 +24,5 @@ export declare const defaultStackParser: StackParser; export declare function close(timeout?: number | undefined): PromiseLike; export declare function flush(timeout?: number | undefined): PromiseLike; export declare function lastEventId(): string | undefined; + +export default sentryAstro; From 921b8df15d9a1b6f4fb3c000ddf3df7e08b48642 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 24 Oct 2023 10:00:33 +0200 Subject: [PATCH 35/38] fix(replay): Fix xhr start timestamps (#9341) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We've been using the wrong `startTimestamp` for the core xhr instrumentation. Outside of replay, this wasn't noticed because we are not actually using the anywhere 😬 But in replay, it lead to all xhr breadrcumbs showing an incorrect duration of `0`. Note that this is _maybe_ not 100% correct, as in theory you could call `xhr.send()` later, which is probably the _most correct_ start timestamp. But this would require us to keep the start time somewhere on the xhr object, which is a bit trickier than this solution. So I think it is fine to do this based on `xhr.open()` (and _definitely_ more correct than it was before). fixes https://github.com/getsentry/sentry/issues/52790 --- .../fetch/captureTimestamps/init.js | 18 +++++ .../fetch/captureTimestamps/test.ts | 67 +++++++++++++++++ .../xhr/captureTimestamps/init.js | 18 +++++ .../xhr/captureTimestamps/test.ts | 75 +++++++++++++++++++ packages/utils/src/instrument.ts | 4 +- 5 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/init.js create mode 100644 packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/test.ts create mode 100644 packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/init.js create mode 100644 packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/test.ts diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/init.js b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/init.js new file mode 100644 index 000000000000..52c219e99dc9 --- /dev/null +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/init.js @@ -0,0 +1,18 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window.Replay = new Sentry.Replay({ + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, +}); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1, + // We ensure to sample for errors, so by default nothing is sent + replaysSessionSampleRate: 0.0, + replaysOnErrorSampleRate: 1.0, + + integrations: [window.Replay], +}); diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/test.ts new file mode 100644 index 000000000000..203a89caaaab --- /dev/null +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/test.ts @@ -0,0 +1,67 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../../utils/fixtures'; +import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers'; +import { + getCustomRecordingEvents, + shouldSkipReplayTest, + waitForReplayRequest, +} from '../../../../../utils/replayHelpers'; + +sentryTest('captures correct timestamps', async ({ getLocalTestPath, page, browserName }) => { + // These are a bit flaky on non-chromium browsers + if (shouldSkipReplayTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + await page.route('**/foo', route => { + return route.fulfill({ + status: 200, + }); + }); + + await page.route('https://dsn.ingest.sentry.io/**/*', async route => { + await new Promise(resolve => setTimeout(resolve, 10)); + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const requestPromise = waitForErrorRequest(page); + const replayRequestPromise1 = waitForReplayRequest(page, 0); + + const url = await getLocalTestPath({ testDir: __dirname }); + await page.goto(url); + + await page.evaluate(() => { + /* eslint-disable */ + fetch('http://localhost:7654/foo', { + method: 'POST', + body: '{"foo":"bar"}', + }).then(() => { + // @ts-expect-error Sentry is a global + Sentry.captureException('test error'); + }); + /* eslint-enable */ + }); + + const request = await requestPromise; + const eventData = envelopeRequestParser(request); + + const replayReq1 = await replayRequestPromise1; + const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); + + const xhrSpan = performanceSpans1.find(span => span.op === 'resource.fetch')!; + + expect(xhrSpan).toBeDefined(); + + const { startTimestamp, endTimestamp } = xhrSpan; + + expect(startTimestamp).toEqual(expect.any(Number)); + expect(endTimestamp).toEqual(expect.any(Number)); + expect(endTimestamp).toBeGreaterThan(startTimestamp); + + expect(eventData!.breadcrumbs![0].timestamp).toBeGreaterThan(startTimestamp); +}); diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/init.js b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/init.js new file mode 100644 index 000000000000..52c219e99dc9 --- /dev/null +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/init.js @@ -0,0 +1,18 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window.Replay = new Sentry.Replay({ + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, +}); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1, + // We ensure to sample for errors, so by default nothing is sent + replaysSessionSampleRate: 0.0, + replaysOnErrorSampleRate: 1.0, + + integrations: [window.Replay], +}); diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/test.ts new file mode 100644 index 000000000000..1a60ceea6509 --- /dev/null +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/test.ts @@ -0,0 +1,75 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../../utils/fixtures'; +import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers'; +import { + getCustomRecordingEvents, + shouldSkipReplayTest, + waitForReplayRequest, +} from '../../../../../utils/replayHelpers'; + +sentryTest('captures correct timestamps', async ({ getLocalTestPath, page, browserName }) => { + // These are a bit flaky on non-chromium browsers + if (shouldSkipReplayTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + await page.route('**/foo', route => { + return route.fulfill({ + status: 200, + }); + }); + + await page.route('https://dsn.ingest.sentry.io/**/*', async route => { + await new Promise(resolve => setTimeout(resolve, 10)); + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const requestPromise = waitForErrorRequest(page); + const replayRequestPromise1 = waitForReplayRequest(page, 0); + + const url = await getLocalTestPath({ testDir: __dirname }); + await page.goto(url); + + void page.evaluate(() => { + /* eslint-disable */ + const xhr = new XMLHttpRequest(); + + xhr.open('POST', 'http://localhost:7654/foo'); + xhr.setRequestHeader('Accept', 'application/json'); + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.setRequestHeader('Cache', 'no-cache'); + xhr.setRequestHeader('X-Test-Header', 'test-value'); + xhr.send(); + + xhr.addEventListener('readystatechange', function () { + if (xhr.readyState === 4) { + // @ts-expect-error Sentry is a global + setTimeout(() => Sentry.captureException('test error', 0)); + } + }); + /* eslint-enable */ + }); + + const request = await requestPromise; + const eventData = envelopeRequestParser(request); + + const replayReq1 = await replayRequestPromise1; + const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); + + const xhrSpan = performanceSpans1.find(span => span.op === 'resource.xhr')!; + + expect(xhrSpan).toBeDefined(); + + const { startTimestamp, endTimestamp } = xhrSpan; + + expect(startTimestamp).toEqual(expect.any(Number)); + expect(endTimestamp).toEqual(expect.any(Number)); + expect(endTimestamp).toBeGreaterThan(startTimestamp); + + expect(eventData!.breadcrumbs![0].timestamp).toBeGreaterThan(startTimestamp); +}); diff --git a/packages/utils/src/instrument.ts b/packages/utils/src/instrument.ts index 99ddc3aaefc8..6d85ec2036fc 100644 --- a/packages/utils/src/instrument.ts +++ b/packages/utils/src/instrument.ts @@ -257,6 +257,8 @@ export function instrumentXHR(): void { fill(xhrproto, 'open', function (originalOpen: () => void): () => void { return function (this: XMLHttpRequest & SentryWrappedXMLHttpRequest, ...args: any[]): void { + const startTimestamp = Date.now(); + const url = args[1]; const xhrInfo: SentryXhrData = (this[SENTRY_XHR_DATA_KEY] = { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access @@ -291,7 +293,7 @@ export function instrumentXHR(): void { triggerHandlers('xhr', { args: args as [string, string], endTimestamp: Date.now(), - startTimestamp: Date.now(), + startTimestamp, xhr: this, } as HandlerDataXhr); } From a0ff516cbbb8acf1855587cf5829a9ee39cb63aa Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 24 Oct 2023 10:53:14 +0200 Subject: [PATCH 36/38] feat(replay): Share performance instrumentation with tracing (#9296) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This streamlines web-vital & performance observer handling, by exposing a new `addPerformanceInstrumentationHandler` method from `@sentry-internal/tracing`. This works similar to the instrumentation in utils, where the first time you add instrumentation for a given type, it will add a performance observer. And any further calls will just add more callbacks. This way, we avoid having multiple of the same performance observers. Furthermore, this also aligns the handling of LCP capturing for replay. We used to do this separately, now we use the same data as for performance. Finally, while doing this I noticed that a whole bunch of performance observer stuff we used to capture in Replay, was actually discarded 😬 so no need to capture these anymore at all. (We can always add it back later, if needed) Some integration tests needed slight adjustments for this, probably due to minor timing semantics. But I think all the changes are good/"correct". I _also_ got rid of the event deduplication in replay. Closes https://github.com/getsentry/sentry-javascript/issues/9246 --- .../scripts/detectFlakyTests.ts | 1 - .../suites/replay/eventBufferError/test.ts | 5 +- .../defaultOptions/template.html | 7 +- .../largeMutations/defaultOptions/test.ts | 38 +- .../mutationLimit/template.html | 7 +- .../largeMutations/mutationLimit/test.ts | 23 +- .../metrics/web-vitals-lcp/template.html | 1 + .../tracing/metrics/web-vitals-lcp/test.ts | 8 +- .../utils/replayHelpers.ts | 2 +- .../tests/behaviour-test.spec.ts | 13 +- .../tests/fixtures/ReplayRecordingData.ts | 428 +++++++++--------- .../tests/behaviour-test.spec.ts | 13 +- .../tests/fixtures/ReplayRecordingData.ts | 428 +++++++++--------- .../tests/behaviour-test.spec.ts | 13 +- .../tests/fixtures/ReplayRecordingData.ts | 428 +++++++++--------- packages/replay/package.json | 1 + .../src/coreHandlers/performanceObserver.ts | 63 ++- packages/replay/src/replay.ts | 40 +- packages/replay/src/types/replay.ts | 12 +- .../src/util/createPerformanceEntries.ts | 49 +- .../src/util/dedupePerformanceEntries.ts | 90 ---- .../test/fixtures/performanceEntry/lcp.ts | 19 - .../test/integration/errorSampleRate.test.ts | 2 +- .../replay/test/integration/events.test.ts | 2 +- .../unit/util/createPerformanceEntry.test.ts | 31 +- .../util/dedupePerformanceEntries.test.ts | 67 --- .../tracing-internal/src/browser/index.ts | 7 + .../src/browser/instrument.ts | 243 ++++++++++ .../src/browser/metrics/index.ts | 48 +- .../tracing-internal/src/browser/request.ts | 17 +- packages/tracing-internal/src/index.ts | 4 + packages/utils/src/browser.ts | 4 + packages/utils/test/browser.test.ts | 4 + 33 files changed, 1108 insertions(+), 1010 deletions(-) delete mode 100644 packages/replay/src/util/dedupePerformanceEntries.ts delete mode 100644 packages/replay/test/fixtures/performanceEntry/lcp.ts delete mode 100644 packages/replay/test/unit/util/dedupePerformanceEntries.test.ts create mode 100644 packages/tracing-internal/src/browser/instrument.ts diff --git a/packages/browser-integration-tests/scripts/detectFlakyTests.ts b/packages/browser-integration-tests/scripts/detectFlakyTests.ts index 12d83f30af5b..9fcf2baea7f9 100644 --- a/packages/browser-integration-tests/scripts/detectFlakyTests.ts +++ b/packages/browser-integration-tests/scripts/detectFlakyTests.ts @@ -1,7 +1,6 @@ import * as glob from 'glob'; import * as path from 'path'; import * as childProcess from 'child_process'; -import { promisify } from 'util'; async function run(): Promise { let testPaths: string[] = []; diff --git a/packages/browser-integration-tests/suites/replay/eventBufferError/test.ts b/packages/browser-integration-tests/suites/replay/eventBufferError/test.ts index 10e9ad6f7196..954d257bf202 100644 --- a/packages/browser-integration-tests/suites/replay/eventBufferError/test.ts +++ b/packages/browser-integration-tests/suites/replay/eventBufferError/test.ts @@ -5,6 +5,7 @@ import { envelopeRequestParser } from '../../../utils/helpers'; import { getDecompressedRecordingEvents, getReplaySnapshot, + isCustomSnapshot, isReplayEvent, REPLAY_DEFAULT_FLUSH_MAX_DELAY, shouldSkipReplayTest, @@ -41,8 +42,8 @@ sentryTest( // We only want to count replays here if (event && isReplayEvent(event)) { const events = getDecompressedRecordingEvents(route.request()); - // this makes sure we ignore e.g. mouse move events which can otherwise lead to flakes - if (events.length > 0) { + // Make sure to not count mouse moves or performance spans + if (events.filter(event => !isCustomSnapshot(event) || event.data.tag !== 'performanceSpan').length > 0) { called++; } } diff --git a/packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/template.html b/packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/template.html index 331a03a58b92..bec4cdcb8e94 100644 --- a/packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/template.html +++ b/packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/template.html @@ -4,9 +4,10 @@ - - - + + + +