Skip to content
Open
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
6 changes: 4 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ jobs:
- name: Set up Deno
uses: denoland/setup-deno@v2.0.4
with:
deno-version: v2.1.5
deno-version: v2.7.14
- name: Restore caches
uses: ./.github/actions/restore-cache
with:
Expand Down Expand Up @@ -986,7 +986,9 @@ jobs:
use-installer: true
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Deno
if: matrix.test-application == 'deno' || matrix.test-application == 'deno-streamed'
if:
matrix.test-application == 'deno' || matrix.test-application == 'deno-streamed' || matrix.test-application ==
'deno-redis'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E2E CI uses outdated Deno version for deno-redis

High Severity

The E2E test job sets deno-version: v2.1.5 for deno-redis, but the denoRedisIntegration and the test app explicitly require Deno 2.7.13+ (as stated in the app comment at line 15 of app.ts). The unit test job was correctly updated to v2.7.14 at line 530, but the E2E test job was not. The deno-redis E2E tests will likely fail or produce incorrect results running on a Deno version that is too old.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 72be1fa. Configure here.

uses: denoland/setup-deno@v2.0.4
with:
deno-version: v2.1.5
Expand Down
7 changes: 7 additions & 0 deletions dev-packages/e2e-tests/test-applications/deno-redis/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"imports": {
"@sentry/deno": "npm:@sentry/deno",
"redis": "npm:redis@^5.12.0"
},
"nodeModulesDir": "manual"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
services:
redis:
image: redis:8
restart: always
container_name: e2e-tests-deno-redis
ports:
- '6379:6379'
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 1s
timeout: 3s
retries: 30
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { execSync } from 'child_process';
import { dirname } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));

export default async function globalSetup() {
// Start Redis via Docker Compose. `--wait` blocks until the healthcheck
// in docker-compose.yml passes, so the Deno app can connect immediately.
execSync('docker compose up -d --wait', {
cwd: __dirname,
stdio: 'inherit',
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { execSync } from 'child_process';
import { dirname } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));

export default async function globalTeardown() {
execSync('docker compose down --volumes', {
cwd: __dirname,
stdio: 'inherit',
});
}
22 changes: 22 additions & 0 deletions dev-packages/e2e-tests/test-applications/deno-redis/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "deno-redis",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "deno run --allow-net --allow-env --allow-read --allow-sys --allow-write src/app.ts",
"test": "playwright test",
"clean": "npx rimraf node_modules pnpm-lock.yaml",
"test:build": "pnpm install",
"test:assert": "pnpm test"
},
"dependencies": {
"@sentry/deno": "file:../../packed/sentry-deno-packed.tgz"
},
"devDependencies": {
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
"volta": {
"extends": "../../package.json"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { getPlaywrightConfig } from '@sentry-internal/test-utils';

const config = getPlaywrightConfig({
startCommand: `pnpm start`,
port: 3030,
});

export default {
...config,
globalSetup: './global-setup.mjs',
globalTeardown: './global-teardown.mjs',
};
54 changes: 54 additions & 0 deletions dev-packages/e2e-tests/test-applications/deno-redis/src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as Sentry from '@sentry/deno';
import { createClient } from 'redis';

Sentry.init({
environment: 'qa',
dsn: Deno.env.get('E2E_TEST_DSN'),
debug: !!Deno.env.get('DEBUG'),
tunnel: 'http://localhost:3031/',
tracesSampleRate: 1,
});

// One shared client per process. node-redis publishes to the
// `node-redis:command` / `:batch` / `:connect` diagnostics channels for every
// operation on this client; denoRedisIntegration is already subscribed to
// those (it's part of the default integrations on Deno 2.7.13+).
const redis = createClient({
url: Deno.env.get('REDIS_URL') ?? 'redis://127.0.0.1:6379',
});
redis.on('error', err => {
// eslint-disable-next-line no-console
console.error('redis client error', err);
});
await redis.connect();

const port = 3030;

Deno.serve({ port, hostname: '0.0.0.0' }, async (req: Request) => {
const url = new URL(req.url);

// GET — exercises the command channel, success path.
if (url.pathname === '/redis-get') {
const key = url.searchParams.get('key') ?? 'cache:key';
const value = await redis.get(key);
return Response.json({ key, value });
}

// SET then GET — exercises two commands inside a single transaction so we
// can assert the parent has two db.redis children.
if (url.pathname === '/redis-set-get') {
const key = url.searchParams.get('key') ?? 'cache:key';
const value = url.searchParams.get('value') ?? 'hello';
await redis.set(key, value);
const echoed = await redis.get(key);
return Response.json({ key, value: echoed });
}

// MULTI — exercises the batch channel.
if (url.pathname === '/redis-multi') {
const result = await redis.multi().set('multi:a', '1').set('multi:b', '2').get('multi:a').exec();
return Response.json({ result });
}

return new Response('Not found', { status: 404 });
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { startEventProxyServer } from '@sentry-internal/test-utils';

startEventProxyServer({
port: 3031,
proxyServerName: 'deno-redis',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';

test('GET command emits an http.server transaction containing a db.redis child span', async ({ baseURL }) => {
// Each incoming request gets a Sentry http.server transaction (via the
// default denoServeIntegration); the redis command runs inside it, so the
// child span attaches to that transaction.
const transactionPromise = waitForTransaction('deno-redis', event => {
return (
event?.contexts?.trace?.op === 'http.server' &&
(event.request?.url ?? '').includes('/redis-get') &&
(event.spans?.some(span => span.op === 'db.redis') ?? false)
);
});

const res = await fetch(`${baseURL}/redis-get?key=cache:user:42`);
expect(res.status).toBe(200);
await res.json();

const transaction = await transactionPromise;
const redisSpan = transaction.spans!.find(span => span.op === 'db.redis');
expect(redisSpan).toBeDefined();
expect(redisSpan!.description).toBe('redis-GET');
expect(redisSpan!.data?.['db.system']).toBe('redis');
// Statement omits the value; for GET the only allowed arg is the key.
expect(redisSpan!.data?.['db.statement']).toBe('GET cache:user:42');
expect(redisSpan!.data?.['net.peer.port']).toBe(6379);
});

test('SET then GET emit two db.redis child spans on the same transaction', async ({ baseURL }) => {
const transactionPromise = waitForTransaction('deno-redis', event => {
return (
event?.contexts?.trace?.op === 'http.server' &&
(event.request?.url ?? '').includes('/redis-set-get') &&
(event.spans?.filter(span => span.op === 'db.redis').length ?? 0) >= 2
);
});

const res = await fetch(`${baseURL}/redis-set-get?key=cache:greeting&value=hello`);
expect(res.status).toBe(200);
await res.json();

const transaction = await transactionPromise;
const redisSpans = transaction.spans!.filter(span => span.op === 'db.redis');
expect(redisSpans.length).toBeGreaterThanOrEqual(2);
const ops = redisSpans.map(s => s.description);
expect(ops).toContain('redis-SET');
expect(ops).toContain('redis-GET');
});

test('MULTI batch emits a PIPELINE/MULTI batch span', async ({ baseURL }) => {
const transactionPromise = waitForTransaction('deno-redis', event => {
return (
event?.contexts?.trace?.op === 'http.server' &&
(event.request?.url ?? '').includes('/redis-multi') &&
(event.spans?.some(span => span.description === 'MULTI' || span.description === 'PIPELINE') ?? false)
);
});

const res = await fetch(`${baseURL}/redis-multi`);
expect(res.status).toBe(200);
await res.json();

const transaction = await transactionPromise;
const batchSpan = transaction.spans!.find(span => span.description === 'MULTI' || span.description === 'PIPELINE');
expect(batchSpan).toBeDefined();
expect(batchSpan!.op).toBe('db.redis');
expect(batchSpan!.data?.['db.system']).toBe('redis');
});
Loading
Loading