Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
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<Response>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
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<string, string> = {
orders: '/api/queues/process-order',
};

// The file path key used in vercel.json for each consumer route.
const ROUTE_FILE_PATHS: Record<string, string> = {
'/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 });
}
12 changes: 12 additions & 0 deletions dev-packages/e2e-tests/test-applications/nextjs-16/lib/queue.ts
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,10 @@
"path": "/api/cron-test-error",
"schedule": "30 * * * *"
}
]
],
"functions": {
"app/api/queues/process-order/route.ts": {
"experimentalTriggers": [{ "type": "queue/v2beta", "topic": "orders" }]
}
}
}
Loading