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
8 changes: 5 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,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 @@ -996,10 +996,12 @@ 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'
Comment thread
cursor[bot] marked this conversation as resolved.
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
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',
});
}
23 changes: 23 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,23 @@
{
"name": "deno-redis",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "docker compose up -d --wait && 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",
"redis": "^5.12.0"
},
"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',
};
61 changes: 61 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,61 @@
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.
const redis = createClient({
url: Deno.env.get('REDIS_URL') ?? 'redis://127.0.0.1:6379',
});
function onRedisError(err: unknown) {
// eslint-disable-next-line no-console
console.error('redis client error', err);
}
redis.on('error', onRedisError);
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 });
}

if (url.pathname === '/redis-disconnect') {
redis.off('error', onRedisError);
redis.close();
return new Response('ok');
}

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,74 @@
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');
});

test('shut down redis client', async ({ baseURL }) => {
const res = await fetch(`${baseURL}/redis-disconnect`);
expect(await res.text()).toBe('ok');
});
Loading
Loading