diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/metrics/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16/app/metrics/page.tsx
new file mode 100644
index 000000000000..fdb7bc0a40a7
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/metrics/page.tsx
@@ -0,0 +1,34 @@
+'use client';
+
+import * as Sentry from '@sentry/nextjs';
+
+export default function Page() {
+ const handleClick = async () => {
+ Sentry.metrics.count('test.page.count', 1, {
+ attributes: {
+ page: '/metrics',
+ 'random.attribute': 'Apples',
+ },
+ });
+ Sentry.metrics.distribution('test.page.distribution', 100, {
+ attributes: {
+ page: '/metrics',
+ 'random.attribute': 'Manzanas',
+ },
+ });
+ Sentry.metrics.gauge('test.page.gauge', 200, {
+ attributes: {
+ page: '/metrics',
+ 'random.attribute': 'Mele',
+ },
+ });
+ await fetch('/metrics/route-handler');
+ };
+
+ return (
+
+
Metrics page
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/metrics/route-handler/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/metrics/route-handler/route.ts
new file mode 100644
index 000000000000..84e81960f9c9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/metrics/route-handler/route.ts
@@ -0,0 +1,23 @@
+import * as Sentry from '@sentry/nextjs';
+
+export const GET = async () => {
+ Sentry.metrics.count('test.route.handler.count', 1, {
+ attributes: {
+ endpoint: '/metrics/route-handler',
+ 'random.attribute': 'Potatoes',
+ },
+ });
+ Sentry.metrics.distribution('test.route.handler.distribution', 100, {
+ attributes: {
+ endpoint: '/metrics/route-handler',
+ 'random.attribute': 'Patatas',
+ },
+ });
+ Sentry.metrics.gauge('test.route.handler.gauge', 200, {
+ attributes: {
+ endpoint: '/metrics/route-handler',
+ 'random.attribute': 'Patate',
+ },
+ });
+ return Response.json({ message: 'Bueno' });
+};
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/metrics.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/metrics.test.ts
new file mode 100644
index 000000000000..43edb917d526
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/metrics.test.ts
@@ -0,0 +1,133 @@
+import { expect, test } from '@playwright/test';
+import { waitForMetric } from '@sentry-internal/test-utils';
+
+test('Should emit metrics from server and client', async ({ request, page }) => {
+ const clientCountPromise = waitForMetric('nextjs-16', async metric => {
+ return metric.name === 'test.page.count';
+ });
+
+ const clientDistributionPromise = waitForMetric('nextjs-16', async metric => {
+ return metric.name === 'test.page.distribution';
+ });
+
+ const clientGaugePromise = waitForMetric('nextjs-16', async metric => {
+ return metric.name === 'test.page.gauge';
+ });
+
+ const serverCountPromise = waitForMetric('nextjs-16', async metric => {
+ return metric.name === 'test.route.handler.count';
+ });
+
+ const serverDistributionPromise = waitForMetric('nextjs-16', async metric => {
+ return metric.name === 'test.route.handler.distribution';
+ });
+
+ const serverGaugePromise = waitForMetric('nextjs-16', async metric => {
+ return metric.name === 'test.route.handler.gauge';
+ });
+
+ await page.goto('/metrics');
+ await page.getByText('Emit').click();
+ const clientCount = await clientCountPromise;
+ const clientDistribution = await clientDistributionPromise;
+ const clientGauge = await clientGaugePromise;
+ const serverCount = await serverCountPromise;
+ const serverDistribution = await serverDistributionPromise;
+ const serverGauge = await serverGaugePromise;
+
+ expect(clientCount).toMatchObject({
+ timestamp: expect.any(Number),
+ trace_id: expect.any(String),
+ span_id: expect.any(String),
+ name: 'test.page.count',
+ type: 'counter',
+ value: 1,
+ attributes: {
+ page: { value: '/metrics', type: 'string' },
+ 'random.attribute': { value: 'Apples', type: 'string' },
+ 'sentry.environment': { value: 'qa', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' },
+ 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
+ },
+ });
+
+ expect(clientDistribution).toMatchObject({
+ timestamp: expect.any(Number),
+ trace_id: expect.any(String),
+ span_id: expect.any(String),
+ name: 'test.page.distribution',
+ type: 'distribution',
+ value: 100,
+ attributes: {
+ page: { value: '/metrics', type: 'string' },
+ 'random.attribute': { value: 'Manzanas', type: 'string' },
+ 'sentry.environment': { value: 'qa', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' },
+ 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
+ },
+ });
+
+ expect(clientGauge).toMatchObject({
+ timestamp: expect.any(Number),
+ trace_id: expect.any(String),
+ span_id: expect.any(String),
+ name: 'test.page.gauge',
+ type: 'gauge',
+ value: 200,
+ attributes: {
+ page: { value: '/metrics', type: 'string' },
+ 'random.attribute': { value: 'Mele', type: 'string' },
+ 'sentry.environment': { value: 'qa', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' },
+ 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
+ },
+ });
+
+ expect(serverCount).toMatchObject({
+ timestamp: expect.any(Number),
+ trace_id: expect.any(String),
+ name: 'test.route.handler.count',
+ type: 'counter',
+ value: 1,
+ attributes: {
+ 'server.address': { value: expect.any(String), type: 'string' },
+ 'random.attribute': { value: 'Potatoes', type: 'string' },
+ endpoint: { value: '/metrics/route-handler', type: 'string' },
+ 'sentry.environment': { value: 'qa', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' },
+ 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
+ },
+ });
+
+ expect(serverDistribution).toMatchObject({
+ timestamp: expect.any(Number),
+ trace_id: expect.any(String),
+ name: 'test.route.handler.distribution',
+ type: 'distribution',
+ value: 100,
+ attributes: {
+ 'server.address': { value: expect.any(String), type: 'string' },
+ 'random.attribute': { value: 'Patatas', type: 'string' },
+ endpoint: { value: '/metrics/route-handler', type: 'string' },
+ 'sentry.environment': { value: 'qa', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' },
+ 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
+ },
+ });
+
+ expect(serverGauge).toMatchObject({
+ timestamp: expect.any(Number),
+ trace_id: expect.any(String),
+ name: 'test.route.handler.gauge',
+ type: 'gauge',
+ value: 200,
+ attributes: {
+ 'server.address': { value: expect.any(String), type: 'string' },
+ 'random.attribute': { value: 'Patate', type: 'string' },
+ endpoint: { value: '/metrics/route-handler', type: 'string' },
+ 'sentry.environment': { value: 'qa', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' },
+ 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
+ },
+ });
+});
diff --git a/dev-packages/test-utils/src/event-proxy-server.ts b/dev-packages/test-utils/src/event-proxy-server.ts
index 08fa39db950f..9c411c3fc015 100644
--- a/dev-packages/test-utils/src/event-proxy-server.ts
+++ b/dev-packages/test-utils/src/event-proxy-server.ts
@@ -1,5 +1,12 @@
/* eslint-disable max-lines */
-import type { Envelope, EnvelopeItem, Event, SerializedSession } from '@sentry/core';
+import type {
+ Envelope,
+ EnvelopeItem,
+ Event,
+ SerializedMetric,
+ SerializedMetricContainer,
+ SerializedSession,
+} from '@sentry/core';
import { parseEnvelope } from '@sentry/core';
import * as fs from 'fs';
import * as http from 'http';
@@ -391,6 +398,35 @@ export function waitForTransaction(
});
}
+/**
+ * Wait for metric items to be sent.
+ */
+export function waitForMetric(
+ proxyServerName: string,
+ callback: (metricEvent: SerializedMetric) => Promise | boolean,
+): Promise {
+ const timestamp = getNanosecondTimestamp();
+ return new Promise((resolve, reject) => {
+ waitForEnvelopeItem(
+ proxyServerName,
+ async envelopeItem => {
+ const [envelopeItemHeader, envelopeItemBody] = envelopeItem;
+ const metricContainer = envelopeItemBody as SerializedMetricContainer;
+ if (envelopeItemHeader.type === 'trace_metric') {
+ for (const metric of metricContainer.items) {
+ if (await callback(metric)) {
+ resolve(metric);
+ return true;
+ }
+ }
+ }
+ return false;
+ },
+ timestamp,
+ ).catch(reject);
+ });
+}
+
const TEMP_FILE_PREFIX = 'event-proxy-server-';
async function registerCallbackServerPort(serverName: string, port: string): Promise {
diff --git a/dev-packages/test-utils/src/index.ts b/dev-packages/test-utils/src/index.ts
index e9ae76f592ed..b14248aabd95 100644
--- a/dev-packages/test-utils/src/index.ts
+++ b/dev-packages/test-utils/src/index.ts
@@ -7,6 +7,7 @@ export {
waitForTransaction,
waitForSession,
waitForPlainRequest,
+ waitForMetric,
} from './event-proxy-server';
export { getPlaywrightConfig } from './playwright-config';