diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts index 8f0b4d0f7800..d7015bce4a30 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts @@ -8,7 +8,7 @@ Sentry.init({ tracesSampleRate: 1.0, sendDefaultPii: true, // debug: true, - integrations: [Sentry.vercelAIIntegration()], + integrations: [Sentry.vercelAIIntegration(), Sentry.nodeRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 })], // Verify Log type is available beforeSendLog(log: Log) { return log; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/node-runtime-metrics.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/node-runtime-metrics.test.ts new file mode 100644 index 000000000000..0efd0d8f7d79 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/node-runtime-metrics.test.ts @@ -0,0 +1,148 @@ +import { expect, test } from '@playwright/test'; +import { waitForMetric } from '@sentry-internal/test-utils'; + +const EXPECTED_ATTRIBUTES = { + 'sentry.environment': { value: 'qa', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.origin': { value: 'auto.node.runtime_metrics', type: 'string' }, +}; + +test('Should emit node runtime memory metrics', async ({ request }) => { + const rssPromise = waitForMetric('nextjs-16', metric => { + return metric.name === 'node.runtime.mem.rss'; + }); + + const heapUsedPromise = waitForMetric('nextjs-16', metric => { + return metric.name === 'node.runtime.mem.heap_used'; + }); + + const heapTotalPromise = waitForMetric('nextjs-16', metric => { + return metric.name === 'node.runtime.mem.heap_total'; + }); + + // Trigger a request to ensure the server is running and metrics start being collected + await request.get('/'); + + const rss = await rssPromise; + const heapUsed = await heapUsedPromise; + const heapTotal = await heapTotalPromise; + + expect(rss).toMatchObject({ + timestamp: expect.any(Number), + trace_id: expect.any(String), + name: 'node.runtime.mem.rss', + type: 'gauge', + unit: 'byte', + value: expect.any(Number), + attributes: expect.objectContaining(EXPECTED_ATTRIBUTES), + }); + + expect(heapUsed).toMatchObject({ + timestamp: expect.any(Number), + trace_id: expect.any(String), + name: 'node.runtime.mem.heap_used', + type: 'gauge', + unit: 'byte', + value: expect.any(Number), + attributes: expect.objectContaining(EXPECTED_ATTRIBUTES), + }); + + expect(heapTotal).toMatchObject({ + timestamp: expect.any(Number), + trace_id: expect.any(String), + name: 'node.runtime.mem.heap_total', + type: 'gauge', + unit: 'byte', + value: expect.any(Number), + attributes: expect.objectContaining(EXPECTED_ATTRIBUTES), + }); +}); + +test('Should emit node runtime CPU utilization metric', async ({ request }) => { + const cpuUtilPromise = waitForMetric('nextjs-16', metric => { + return metric.name === 'node.runtime.cpu.utilization'; + }); + + await request.get('/'); + + const cpuUtil = await cpuUtilPromise; + + expect(cpuUtil).toMatchObject({ + timestamp: expect.any(Number), + trace_id: expect.any(String), + name: 'node.runtime.cpu.utilization', + type: 'gauge', + value: expect.any(Number), + attributes: expect.objectContaining(EXPECTED_ATTRIBUTES), + }); +}); + +test('Should emit node runtime event loop metrics', async ({ request }) => { + const elDelayP50Promise = waitForMetric('nextjs-16', metric => { + return metric.name === 'node.runtime.event_loop.delay.p50'; + }); + + const elDelayP99Promise = waitForMetric('nextjs-16', metric => { + return metric.name === 'node.runtime.event_loop.delay.p99'; + }); + + const elUtilPromise = waitForMetric('nextjs-16', metric => { + return metric.name === 'node.runtime.event_loop.utilization'; + }); + + await request.get('/'); + + const elDelayP50 = await elDelayP50Promise; + const elDelayP99 = await elDelayP99Promise; + const elUtil = await elUtilPromise; + + expect(elDelayP50).toMatchObject({ + timestamp: expect.any(Number), + trace_id: expect.any(String), + name: 'node.runtime.event_loop.delay.p50', + type: 'gauge', + unit: 'second', + value: expect.any(Number), + attributes: expect.objectContaining(EXPECTED_ATTRIBUTES), + }); + + expect(elDelayP99).toMatchObject({ + timestamp: expect.any(Number), + trace_id: expect.any(String), + name: 'node.runtime.event_loop.delay.p99', + type: 'gauge', + unit: 'second', + value: expect.any(Number), + attributes: expect.objectContaining(EXPECTED_ATTRIBUTES), + }); + + expect(elUtil).toMatchObject({ + timestamp: expect.any(Number), + trace_id: expect.any(String), + name: 'node.runtime.event_loop.utilization', + type: 'gauge', + value: expect.any(Number), + attributes: expect.objectContaining(EXPECTED_ATTRIBUTES), + }); +}); + +test('Should emit node runtime uptime counter', async ({ request }) => { + const uptimePromise = waitForMetric('nextjs-16', metric => { + return metric.name === 'node.runtime.process.uptime'; + }); + + await request.get('/'); + + const uptime = await uptimePromise; + + expect(uptime).toMatchObject({ + timestamp: expect.any(Number), + trace_id: expect.any(String), + name: 'node.runtime.process.uptime', + type: 'counter', + unit: 'second', + value: expect.any(Number), + attributes: expect.objectContaining(EXPECTED_ATTRIBUTES), + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-express-v5/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express-v5/src/app.ts index 20dfa5bf84c5..9a7f6f07d8bc 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-v5/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-v5/src/app.ts @@ -14,6 +14,7 @@ Sentry.init({ tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, enableLogs: true, + integrations: [Sentry.nodeRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 })], }); import { TRPCError, initTRPC } from '@trpc/server'; diff --git a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/node-runtime-metrics.test.ts b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/node-runtime-metrics.test.ts new file mode 100644 index 000000000000..e8e0aef3be17 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/node-runtime-metrics.test.ts @@ -0,0 +1,148 @@ +import { expect, test } from '@playwright/test'; +import { waitForMetric } from '@sentry-internal/test-utils'; + +const EXPECTED_ATTRIBUTES = { + 'sentry.environment': { value: 'qa', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.origin': { value: 'auto.node.runtime_metrics', type: 'string' }, +}; + +test('Should emit node runtime memory metrics', async ({ baseURL }) => { + const rssPromise = waitForMetric('node-express-v5', metric => { + return metric.name === 'node.runtime.mem.rss'; + }); + + const heapUsedPromise = waitForMetric('node-express-v5', metric => { + return metric.name === 'node.runtime.mem.heap_used'; + }); + + const heapTotalPromise = waitForMetric('node-express-v5', metric => { + return metric.name === 'node.runtime.mem.heap_total'; + }); + + // Trigger a request to ensure the server is running and metrics start being collected + await fetch(`${baseURL}/test-success`); + + const rss = await rssPromise; + const heapUsed = await heapUsedPromise; + const heapTotal = await heapTotalPromise; + + expect(rss).toMatchObject({ + timestamp: expect.any(Number), + trace_id: expect.any(String), + name: 'node.runtime.mem.rss', + type: 'gauge', + unit: 'byte', + value: expect.any(Number), + attributes: expect.objectContaining(EXPECTED_ATTRIBUTES), + }); + + expect(heapUsed).toMatchObject({ + timestamp: expect.any(Number), + trace_id: expect.any(String), + name: 'node.runtime.mem.heap_used', + type: 'gauge', + unit: 'byte', + value: expect.any(Number), + attributes: expect.objectContaining(EXPECTED_ATTRIBUTES), + }); + + expect(heapTotal).toMatchObject({ + timestamp: expect.any(Number), + trace_id: expect.any(String), + name: 'node.runtime.mem.heap_total', + type: 'gauge', + unit: 'byte', + value: expect.any(Number), + attributes: expect.objectContaining(EXPECTED_ATTRIBUTES), + }); +}); + +test('Should emit node runtime CPU utilization metric', async ({ baseURL }) => { + const cpuUtilPromise = waitForMetric('node-express-v5', metric => { + return metric.name === 'node.runtime.cpu.utilization'; + }); + + await fetch(`${baseURL}/test-success`); + + const cpuUtil = await cpuUtilPromise; + + expect(cpuUtil).toMatchObject({ + timestamp: expect.any(Number), + trace_id: expect.any(String), + name: 'node.runtime.cpu.utilization', + type: 'gauge', + value: expect.any(Number), + attributes: expect.objectContaining(EXPECTED_ATTRIBUTES), + }); +}); + +test('Should emit node runtime event loop metrics', async ({ baseURL }) => { + const elDelayP50Promise = waitForMetric('node-express-v5', metric => { + return metric.name === 'node.runtime.event_loop.delay.p50'; + }); + + const elDelayP99Promise = waitForMetric('node-express-v5', metric => { + return metric.name === 'node.runtime.event_loop.delay.p99'; + }); + + const elUtilPromise = waitForMetric('node-express-v5', metric => { + return metric.name === 'node.runtime.event_loop.utilization'; + }); + + await fetch(`${baseURL}/test-success`); + + const elDelayP50 = await elDelayP50Promise; + const elDelayP99 = await elDelayP99Promise; + const elUtil = await elUtilPromise; + + expect(elDelayP50).toMatchObject({ + timestamp: expect.any(Number), + trace_id: expect.any(String), + name: 'node.runtime.event_loop.delay.p50', + type: 'gauge', + unit: 'second', + value: expect.any(Number), + attributes: expect.objectContaining(EXPECTED_ATTRIBUTES), + }); + + expect(elDelayP99).toMatchObject({ + timestamp: expect.any(Number), + trace_id: expect.any(String), + name: 'node.runtime.event_loop.delay.p99', + type: 'gauge', + unit: 'second', + value: expect.any(Number), + attributes: expect.objectContaining(EXPECTED_ATTRIBUTES), + }); + + expect(elUtil).toMatchObject({ + timestamp: expect.any(Number), + trace_id: expect.any(String), + name: 'node.runtime.event_loop.utilization', + type: 'gauge', + value: expect.any(Number), + attributes: expect.objectContaining(EXPECTED_ATTRIBUTES), + }); +}); + +test('Should emit node runtime uptime counter', async ({ baseURL }) => { + const uptimePromise = waitForMetric('node-express-v5', metric => { + return metric.name === 'node.runtime.process.uptime'; + }); + + await fetch(`${baseURL}/test-success`); + + const uptime = await uptimePromise; + + expect(uptime).toMatchObject({ + timestamp: expect.any(Number), + trace_id: expect.any(String), + name: 'node.runtime.process.uptime', + type: 'counter', + unit: 'second', + value: expect.any(Number), + attributes: expect.objectContaining(EXPECTED_ATTRIBUTES), + }); +});