From 5b4bd5b28b2f53cc4fafbb31ca73fe4a96b84bb2 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 13 Mar 2026 13:07:05 +0100 Subject: [PATCH 1/3] add vercel queue tests --- .../nextjs-16/app/api/queue-send/route.ts | 14 +++ .../app/api/queues/process-order/route.ts | 8 ++ .../app/api/v3/topic/[...params]/route.ts | 115 ++++++++++++++++++ .../test-applications/nextjs-16/lib/queue.ts | 12 ++ .../test-applications/nextjs-16/package.json | 1 + .../nextjs-16/tests/vercel-queue.test.ts | 43 +++++++ .../test-applications/nextjs-16/vercel.json | 9 +- 7 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16/app/api/queue-send/route.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16/app/api/queues/process-order/route.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16/app/api/v3/topic/[...params]/route.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16/lib/queue.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16/tests/vercel-queue.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/queue-send/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/queue-send/route.ts new file mode 100644 index 000000000000..fe49a990921b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/queue-send/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from 'next/server'; +import { send } from '../../../lib/queue'; + +export const dynamic = 'force-dynamic'; + +export async function POST(request: Request) { + const body = await request.json(); + const topic = body.topic ?? 'orders'; + const payload = body.payload ?? body; + + const { messageId } = await send(topic, payload); + + return NextResponse.json({ messageId }); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/queues/process-order/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/queues/process-order/route.ts new file mode 100644 index 000000000000..3491fdc07639 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/queues/process-order/route.ts @@ -0,0 +1,8 @@ +import { handleCallback } from '../../../../lib/queue'; + +export const dynamic = 'force-dynamic'; + +export const POST = handleCallback(async (message, _metadata) => { + // Simulate some async work + await new Promise(resolve => setTimeout(resolve, 50)); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/v3/topic/[...params]/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/v3/topic/[...params]/route.ts new file mode 100644 index 000000000000..237cc8f9cc57 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/v3/topic/[...params]/route.ts @@ -0,0 +1,115 @@ +import { NextResponse } from 'next/server'; + +/** + * Mock Vercel Queues API server. + * + * This route handler simulates the Vercel Queues HTTP API so that the real + * @vercel/queue SDK can be used in E2E tests without Vercel infrastructure. + * + * Handled endpoints: + * POST /api/v3/topic/{topic} → SendMessage + * POST /api/v3/topic/{topic}/consumer/{consumer}/id/{messageId} → ReceiveMessageById + * DELETE /api/v3/topic/{topic}/consumer/{consumer}/lease/{handle} → AcknowledgeMessage + * PATCH /api/v3/topic/{topic}/consumer/{consumer}/lease/{handle} → ExtendLease + */ + +export const dynamic = 'force-dynamic'; + +let messageCounter = 0; + +function generateMessageId(): string { + return `msg_test_${++messageCounter}_${Date.now()}`; +} + +function generateReceiptHandle(): string { + return `rh_test_${Date.now()}_${Math.random().toString(36).slice(2)}`; +} + +// Encode a file path into a consumer-group name, matching the SDK's algorithm. +function filePathToConsumerGroup(filePath: string): string { + let result = ''; + for (const char of filePath) { + if (char === '_') result += '__'; + else if (char === '/') result += '_S'; + else if (char === '.') result += '_D'; + else if (/[A-Za-z0-9-]/.test(char)) result += char; + else result += '_' + char.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0'); + } + return result; +} + +// Topic → consumer route path (mirrors vercel.json experimentalTriggers). +const TOPIC_ROUTES: Record = { + orders: '/api/queues/process-order', +}; + +// The file path key used in vercel.json for each consumer route. +const ROUTE_FILE_PATHS: Record = { + '/api/queues/process-order': 'app/api/queues/process-order/route.ts', +}; + +export async function POST(request: Request, { params }: { params: Promise<{ params: string[] }> }) { + const { params: segments } = await params; + + // POST /api/v3/topic/{topic} → SendMessage + if (segments.length === 1) { + const topic = segments[0]; + const body = await request.arrayBuffer(); + const messageId = generateMessageId(); + const receiptHandle = generateReceiptHandle(); + const now = new Date(); + const createdAt = now.toISOString(); + const expiresAt = new Date(now.getTime() + 86_400_000).toISOString(); + const visibilityDeadline = new Date(now.getTime() + 300_000).toISOString(); + + const consumerRoute = TOPIC_ROUTES[topic]; + if (consumerRoute) { + const filePath = ROUTE_FILE_PATHS[consumerRoute] ?? consumerRoute; + const consumerGroup = filePathToConsumerGroup(filePath); + const port = process.env.PORT || 3030; + + // Simulate Vercel infrastructure pushing the message to the consumer. + // Fire-and-forget so the SendMessage response returns immediately. + void fetch(`http://localhost:${port}${consumerRoute}`, { + method: 'POST', + headers: { + 'ce-type': 'com.vercel.queue.v2beta', + 'ce-vqsqueuename': topic, + 'ce-vqsconsumergroup': consumerGroup, + 'ce-vqsmessageid': messageId, + 'ce-vqsreceipthandle': receiptHandle, + 'ce-vqsdeliverycount': '1', + 'ce-vqscreatedat': createdAt, + 'ce-vqsexpiresat': expiresAt, + 'ce-vqsregion': 'test1', + 'ce-vqsvisibilitydeadline': visibilityDeadline, + 'content-type': request.headers.get('content-type') || 'application/json', + }, + body: Buffer.from(body), + }).catch(err => console.error('[mock-queue] Failed to push to consumer:', err)); + } + + return NextResponse.json( + { messageId }, + { status: 201, headers: { 'Vqs-Message-Id': messageId } }, + ); + } + + // POST /api/v3/topic/{topic}/consumer/{consumer}/id/{messageId} → ReceiveMessageById + // Not used in binary-mode push flow, but handled for completeness. + if (segments.length >= 4 && segments[1] === 'consumer') { + return new Response(null, { status: 204 }); + } + + return NextResponse.json({ error: 'Unknown endpoint' }, { status: 404 }); +} + +// DELETE /api/v3/topic/{topic}/consumer/{consumer}/lease/{receiptHandle} → AcknowledgeMessage +export async function DELETE() { + return new Response(null, { status: 204 }); +} + +// PATCH /api/v3/topic/{topic}/consumer/{consumer}/lease/{receiptHandle} → ExtendLease +export async function PATCH() { + return NextResponse.json({ success: true }); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/lib/queue.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/lib/queue.ts new file mode 100644 index 000000000000..8dc8ce0ad5ed --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/lib/queue.ts @@ -0,0 +1,12 @@ +import { QueueClient } from '@vercel/queue'; + +// For E2E testing, point the SDK at a local mock server running within Next.js. +// The mock API lives at app/api/v3/topic/[...params]/route.ts +const queue = new QueueClient({ + region: 'test1', + resolveBaseUrl: () => new URL(`http://localhost:${process.env.PORT || 3030}`), + token: 'mock-token', + deploymentId: null, +}); + +export const { send, handleCallback } = queue; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16/package.json index 262a3ed00c79..fc5613a1b44e 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/package.json @@ -25,6 +25,7 @@ "dependencies": { "@sentry/nextjs": "latest || *", "@sentry/core": "latest || *", + "@vercel/queue": "^0.1.3", "ai": "^3.0.0", "import-in-the-middle": "^2", "next": "16.1.5", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/vercel-queue.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/vercel-queue.test.ts new file mode 100644 index 000000000000..787c06a79e57 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/vercel-queue.test.ts @@ -0,0 +1,43 @@ +import test, { expect } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +// The queue E2E test only runs in production mode. +// In development mode the @vercel/queue SDK uses an in-memory dispatch that +// bypasses our mock HTTP server, causing duplicate handler invocations. +const isProduction = process.env.TEST_ENV === 'production'; + +test('Should create transactions for queue producer and consumer', async ({ request }) => { + test.skip(!isProduction, 'Vercel Queue test only runs in production mode'); + + // 1. Set up waiters for both the producer and consumer transactions. + const producerTransactionPromise = waitForTransaction('nextjs-16', transactionEvent => { + return transactionEvent?.transaction === 'POST /api/queue-send'; + }); + + const consumerTransactionPromise = waitForTransaction('nextjs-16', transactionEvent => { + return transactionEvent?.transaction === 'POST /api/queues/process-order'; + }); + + // 2. Hit the producer route to enqueue a message. + const response = await request.post('/api/queue-send', { + data: { topic: 'orders', payload: { orderId: 'e2e-test-123', action: 'fulfill' } }, + headers: { 'Content-Type': 'application/json' }, + }); + + const responseBody = await response.json(); + expect(response.status()).toBe(200); + expect(responseBody.messageId).toBeTruthy(); + + // 3. Wait for the producer transaction. + const producerTransaction = await producerTransactionPromise; + expect(producerTransaction).toBeDefined(); + expect(producerTransaction.contexts?.trace?.op).toBe('http.server'); + expect(producerTransaction.contexts?.trace?.status).toBe('ok'); + + // 4. Wait for the consumer transaction (the mock server pushes the message + // to the consumer route via CloudEvent POST). + const consumerTransaction = await consumerTransactionPromise; + expect(consumerTransaction).toBeDefined(); + expect(consumerTransaction.contexts?.trace?.op).toBe('http.server'); + expect(consumerTransaction.contexts?.trace?.status).toBe('ok'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/vercel.json b/dev-packages/e2e-tests/test-applications/nextjs-16/vercel.json index b65f0e84701b..0c8506bbf9cb 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/vercel.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/vercel.json @@ -8,5 +8,12 @@ "path": "/api/cron-test-error", "schedule": "30 * * * *" } - ] + ], + "functions": { + "app/api/queues/process-order/route.ts": { + "experimentalTriggers": [ + { "type": "queue/v2beta", "topic": "orders" } + ] + } + } } From ae51cc6142233fbd8b39e950058f85db330b920d Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 13 Mar 2026 13:49:01 +0100 Subject: [PATCH 2/3] fmt --- .../nextjs-16/app/api/v3/topic/[...params]/route.ts | 5 +---- .../e2e-tests/test-applications/nextjs-16/vercel.json | 4 +--- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/v3/topic/[...params]/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/v3/topic/[...params]/route.ts index 237cc8f9cc57..51dfa2e656db 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/v3/topic/[...params]/route.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/v3/topic/[...params]/route.ts @@ -89,10 +89,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ par }).catch(err => console.error('[mock-queue] Failed to push to consumer:', err)); } - return NextResponse.json( - { messageId }, - { status: 201, headers: { 'Vqs-Message-Id': messageId } }, - ); + return NextResponse.json({ messageId }, { status: 201, headers: { 'Vqs-Message-Id': messageId } }); } // POST /api/v3/topic/{topic}/consumer/{consumer}/id/{messageId} → ReceiveMessageById diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/vercel.json b/dev-packages/e2e-tests/test-applications/nextjs-16/vercel.json index 0c8506bbf9cb..58730a0978fb 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/vercel.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/vercel.json @@ -11,9 +11,7 @@ ], "functions": { "app/api/queues/process-order/route.ts": { - "experimentalTriggers": [ - { "type": "queue/v2beta", "topic": "orders" } - ] + "experimentalTriggers": [{ "type": "queue/v2beta", "topic": "orders" }] } } } From 5f8be1eee53aea28d50963ba166eebcd19870d92 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 13 Mar 2026 14:09:02 +0100 Subject: [PATCH 3/3] fix webpack tests --- .../nextjs-16/app/api/queues/process-order/route.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/queues/process-order/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/queues/process-order/route.ts index 3491fdc07639..41cec36d5d8a 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/queues/process-order/route.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/queues/process-order/route.ts @@ -2,7 +2,9 @@ import { handleCallback } from '../../../../lib/queue'; export const dynamic = 'force-dynamic'; +// The @vercel/queue handleCallback return type (CallbackRequestInput) doesn't match +// Next.js's strict route handler type check with webpack builds, so we cast it. export const POST = handleCallback(async (message, _metadata) => { // Simulate some async work await new Promise(resolve => setTimeout(resolve, 50)); -}); +}) as unknown as (req: Request) => Promise;