+
Testing that minReplayDuration is capped at 50s max
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/replay/minReplayDurationLimit/test.ts b/dev-packages/browser-integration-tests/suites/replay/minReplayDurationLimit/test.ts
new file mode 100644
index 000000000000..125af55a6985
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/replay/minReplayDurationLimit/test.ts
@@ -0,0 +1,23 @@
+import { expect } from '@playwright/test';
+import { sentryTest } from '../../../utils/fixtures';
+import { shouldSkipReplayTest } from '../../../utils/replayHelpers';
+
+sentryTest('caps minReplayDuration to maximum of 50 seconds', async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipReplayTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestUrl({ testDir: __dirname });
+
+ await page.goto(url);
+
+ const actualMinReplayDuration = await page.evaluate(() => {
+ // @ts-expect-error - Replay is not typed on window
+ const replayIntegration = window.Replay;
+ const replay = replayIntegration._replay;
+ return replay.getOptions().minReplayDuration;
+ });
+
+ // Even though we configured it to 60s (60000ms), it should be capped to 50s
+ expect(actualMinReplayDuration).toBe(50_000);
+});
diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json
index aac6e9c96945..c791a224a2cc 100644
--- a/dev-packages/cloudflare-integration-tests/package.json
+++ b/dev-packages/cloudflare-integration-tests/package.json
@@ -13,7 +13,8 @@
"test:watch": "yarn test --watch"
},
"dependencies": {
- "@sentry/cloudflare": "10.25.0"
+ "@sentry/cloudflare": "10.25.0",
+ "@langchain/langgraph": "^1.0.1"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20250922.0",
diff --git a/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/index.ts b/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/index.ts
new file mode 100644
index 000000000000..635fcfc8721e
--- /dev/null
+++ b/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/index.ts
@@ -0,0 +1,21 @@
+import * as Sentry from '@sentry/cloudflare';
+
+interface Env {
+ SENTRY_DSN: string;
+}
+
+export default Sentry.withSentry(
+ (env: Env) => ({
+ dsn: env.SENTRY_DSN,
+ release: '1.0.0',
+ environment: 'test',
+ serverName: 'mi-servidor.com',
+ }),
+ {
+ async fetch(_request, _env, _ctx) {
+ Sentry.metrics.count('test.counter', 1, { attributes: { endpoint: '/api/test' } });
+ await Sentry.flush();
+ return new Response('OK');
+ },
+ },
+);
diff --git a/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/test.ts b/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/test.ts
new file mode 100644
index 000000000000..5ee5b0954e59
--- /dev/null
+++ b/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/test.ts
@@ -0,0 +1,50 @@
+import type { SerializedMetricContainer } from '@sentry/core';
+import { expect, it } from 'vitest';
+import { createRunner } from '../../../../runner';
+
+it('should add server.address attribute to metrics when serverName is set', async ({ signal }) => {
+ const runner = createRunner(__dirname)
+ .expect(envelope => {
+ const metric = envelope[1]?.[0]?.[1] as SerializedMetricContainer;
+
+ expect(metric.items[0]).toEqual(
+ expect.objectContaining({
+ name: 'test.counter',
+ type: 'counter',
+ value: 1,
+ span_id: expect.any(String),
+ timestamp: expect.any(Number),
+ trace_id: expect.any(String),
+ attributes: {
+ endpoint: {
+ type: 'string',
+ value: '/api/test',
+ },
+ 'sentry.environment': {
+ type: 'string',
+ value: 'test',
+ },
+ 'sentry.release': {
+ type: 'string',
+ value: expect.any(String),
+ },
+ 'sentry.sdk.name': {
+ type: 'string',
+ value: 'sentry.javascript.cloudflare',
+ },
+ 'sentry.sdk.version': {
+ type: 'string',
+ value: expect.any(String),
+ },
+ 'server.address': {
+ type: 'string',
+ value: 'mi-servidor.com',
+ },
+ },
+ }),
+ );
+ })
+ .start(signal);
+ await runner.makeRequest('get', '/');
+ await runner.completed();
+});
diff --git a/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/wrangler.jsonc
new file mode 100644
index 000000000000..d6be01281f0c
--- /dev/null
+++ b/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/wrangler.jsonc
@@ -0,0 +1,6 @@
+{
+ "name": "worker-name",
+ "compatibility_date": "2025-06-17",
+ "main": "index.ts",
+ "compatibility_flags": ["nodejs_compat"],
+}
diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/index.ts
new file mode 100644
index 000000000000..6837a14be111
--- /dev/null
+++ b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/index.ts
@@ -0,0 +1,66 @@
+import { END, MessagesAnnotation, START, StateGraph } from '@langchain/langgraph';
+import * as Sentry from '@sentry/cloudflare';
+
+interface Env {
+ SENTRY_DSN: string;
+}
+
+export default Sentry.withSentry(
+ (env: Env) => ({
+ dsn: env.SENTRY_DSN,
+ tracesSampleRate: 1.0,
+ sendDefaultPii: true,
+ }),
+ {
+ async fetch(_request, _env, _ctx) {
+ // Define simple mock LLM function
+ const mockLlm = (): {
+ messages: {
+ role: string;
+ content: string;
+ response_metadata: {
+ model_name: string;
+ finish_reason: string;
+ tokenUsage: { promptTokens: number; completionTokens: number; totalTokens: number };
+ };
+ tool_calls: never[];
+ }[];
+ } => {
+ return {
+ messages: [
+ {
+ role: 'assistant',
+ content: 'Mock response from LangGraph agent',
+ response_metadata: {
+ model_name: 'mock-model',
+ finish_reason: 'stop',
+ tokenUsage: {
+ promptTokens: 20,
+ completionTokens: 10,
+ totalTokens: 30,
+ },
+ },
+ tool_calls: [],
+ },
+ ],
+ };
+ };
+
+ // Create and instrument the graph
+ const graph = new StateGraph(MessagesAnnotation)
+ .addNode('agent', mockLlm)
+ .addEdge(START, 'agent')
+ .addEdge('agent', END);
+
+ Sentry.instrumentLangGraph(graph, { recordInputs: true, recordOutputs: true });
+
+ const compiled = graph.compile({ name: 'weather_assistant' });
+
+ await compiled.invoke({
+ messages: [{ role: 'user', content: 'What is the weather in SF?' }],
+ });
+
+ return new Response(JSON.stringify({ success: true }));
+ },
+ },
+);
diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts
new file mode 100644
index 000000000000..33023b30fa55
--- /dev/null
+++ b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts
@@ -0,0 +1,59 @@
+import { expect, it } from 'vitest';
+import { createRunner } from '../../../runner';
+
+// These tests are not exhaustive because the instrumentation is
+// already tested in the node integration tests and we merely
+// want to test that the instrumentation does not break in our
+// cloudflare SDK.
+
+it('traces langgraph compile and invoke operations', async ({ signal }) => {
+ const runner = createRunner(__dirname)
+ .ignore('event')
+ .expect(envelope => {
+ const transactionEvent = envelope[1]?.[0]?.[1] as any;
+
+ expect(transactionEvent.transaction).toBe('GET /');
+
+ // Check create_agent span
+ const createAgentSpan = transactionEvent.spans.find((span: any) => span.op === 'gen_ai.create_agent');
+ expect(createAgentSpan).toMatchObject({
+ data: {
+ 'gen_ai.operation.name': 'create_agent',
+ 'sentry.op': 'gen_ai.create_agent',
+ 'sentry.origin': 'auto.ai.langgraph',
+ 'gen_ai.agent.name': 'weather_assistant',
+ },
+ description: 'create_agent weather_assistant',
+ op: 'gen_ai.create_agent',
+ origin: 'auto.ai.langgraph',
+ });
+
+ // Check invoke_agent span
+ const invokeAgentSpan = transactionEvent.spans.find((span: any) => span.op === 'gen_ai.invoke_agent');
+ expect(invokeAgentSpan).toMatchObject({
+ data: expect.objectContaining({
+ 'gen_ai.operation.name': 'invoke_agent',
+ 'sentry.op': 'gen_ai.invoke_agent',
+ 'sentry.origin': 'auto.ai.langgraph',
+ 'gen_ai.agent.name': 'weather_assistant',
+ 'gen_ai.pipeline.name': 'weather_assistant',
+ 'gen_ai.request.messages': '[{"role":"user","content":"What is the weather in SF?"}]',
+ 'gen_ai.response.model': 'mock-model',
+ 'gen_ai.usage.input_tokens': 20,
+ 'gen_ai.usage.output_tokens': 10,
+ 'gen_ai.usage.total_tokens': 30,
+ }),
+ description: 'invoke_agent weather_assistant',
+ op: 'gen_ai.invoke_agent',
+ origin: 'auto.ai.langgraph',
+ });
+
+ // Verify tools are captured
+ if (invokeAgentSpan.data['gen_ai.request.available_tools']) {
+ expect(invokeAgentSpan.data['gen_ai.request.available_tools']).toMatch(/get_weather/);
+ }
+ })
+ .start(signal);
+ await runner.makeRequest('get', '/');
+ await runner.completed();
+});
diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/wrangler.jsonc
new file mode 100644
index 000000000000..d6be01281f0c
--- /dev/null
+++ b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/wrangler.jsonc
@@ -0,0 +1,6 @@
+{
+ "name": "worker-name",
+ "compatibility_date": "2025-06-17",
+ "main": "index.ts",
+ "compatibility_flags": ["nodejs_compat"],
+}
diff --git a/dev-packages/e2e-tests/test-applications/create-next-app/tests/client-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-next-app/tests/client-transactions.test.ts
index cbb2cae29265..a539216efee7 100644
--- a/dev-packages/e2e-tests/test-applications/create-next-app/tests/client-transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/create-next-app/tests/client-transactions.test.ts
@@ -24,6 +24,7 @@ test('Sends a pageload transaction to Sentry', async ({ page }) => {
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
op: 'pageload',
origin: 'auto.pageload.nextjs.pages_router_instrumentation',
+ status: 'ok',
data: expect.objectContaining({
'sentry.idle_span_finish_reason': 'idleTimeout',
'sentry.op': 'pageload',
@@ -69,6 +70,7 @@ test('captures a navigation transaction to Sentry', async ({ page }) => {
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
op: 'navigation',
origin: 'auto.navigation.nextjs.pages_router_instrumentation',
+ status: 'ok',
data: expect.objectContaining({
'sentry.idle_span_finish_reason': 'idleTimeout',
'sentry.op': 'navigation',
diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/package.json b/dev-packages/e2e-tests/test-applications/ember-embroider/package.json
index 78a2e202d1eb..b7a102917e80 100644
--- a/dev-packages/e2e-tests/test-applications/ember-embroider/package.json
+++ b/dev-packages/e2e-tests/test-applications/ember-embroider/package.json
@@ -68,10 +68,5 @@
},
"volta": {
"extends": "../../package.json"
- },
- "pnpm": {
- "overrides": {
- "@embroider/addon-shim": "1.10.0"
- }
}
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/server-components.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/server-components.test.ts
new file mode 100644
index 000000000000..c9e3a6ff588c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/server-components.test.ts
@@ -0,0 +1,48 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Sends a transaction for a request to app router with URL', async ({ page }) => {
+ const serverComponentTransactionPromise = waitForTransaction('nextjs-13', transactionEvent => {
+ return (
+ transactionEvent?.transaction === 'GET /parameterized/[one]/beep/[two]' &&
+ transactionEvent.contexts?.trace?.data?.['http.target']?.startsWith('/parameterized/1337/beep/42')
+ );
+ });
+
+ await page.goto('/parameterized/1337/beep/42');
+
+ const transactionEvent = await serverComponentTransactionPromise;
+
+ expect(transactionEvent.contexts?.trace).toEqual({
+ data: expect.objectContaining({
+ 'sentry.op': 'http.server',
+ 'sentry.origin': 'auto',
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'route',
+ 'http.method': 'GET',
+ 'http.response.status_code': 200,
+ 'http.route': '/parameterized/[one]/beep/[two]',
+ 'http.status_code': 200,
+ 'http.target': '/parameterized/1337/beep/42',
+ 'otel.kind': 'SERVER',
+ 'next.route': '/parameterized/[one]/beep/[two]',
+ }),
+ op: 'http.server',
+ origin: 'auto',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ status: 'ok',
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ });
+
+ expect(transactionEvent.request).toMatchObject({
+ url: expect.stringContaining('/parameterized/1337/beep/42'),
+ });
+
+ // The transaction should not contain any spans with the same name as the transaction
+ // e.g. "GET /parameterized/[one]/beep/[two]"
+ expect(
+ transactionEvent.spans?.filter(span => {
+ return span.description === transactionEvent.transaction;
+ }),
+ ).toHaveLength(0);
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/isr-test/[product]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15/app/isr-test/[product]/page.tsx
new file mode 100644
index 000000000000..cd1e085e2763
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/isr-test/[product]/page.tsx
@@ -0,0 +1,17 @@
+export const revalidate = 60; // ISR: revalidate every 60 seconds
+export const dynamicParams = true; // Allow dynamic params beyond generateStaticParams
+
+export async function generateStaticParams(): Promise