Skip to content

Commit b4e4854

Browse files
committed
feat(deno): redis diagnostics channel based integration for deno (#21087)
Refactor the redis-dc integration logic into core/src/integrations, and create a Deno integration that uses the same patterns. Instead of the @sentry/opentelemetry/tracing-channel, the Deno integration just adds `_sentrySpan` onto the data in a RedisTracingChannelFactory which is passed to the core utility. Add deno-redis e2e test.
1 parent d75440c commit b4e4854

26 files changed

Lines changed: 992 additions & 296 deletions

File tree

.github/workflows/build.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,7 @@ jobs:
537537
- name: Set up Deno
538538
uses: denoland/setup-deno@v2.0.4
539539
with:
540-
deno-version: v2.1.5
540+
deno-version: v2.7.14
541541
- name: Restore caches
542542
uses: ./.github/actions/restore-cache
543543
with:
@@ -996,10 +996,12 @@ jobs:
996996
use-installer: true
997997
token: ${{ secrets.GITHUB_TOKEN }}
998998
- name: Set up Deno
999-
if: matrix.test-application == 'deno' || matrix.test-application == 'deno-streamed'
999+
if:
1000+
matrix.test-application == 'deno' || matrix.test-application == 'deno-streamed' || matrix.test-application ==
1001+
'deno-redis'
10001002
uses: denoland/setup-deno@v2.0.4
10011003
with:
1002-
deno-version: v2.1.5
1004+
deno-version: v2.7.14
10031005
- name: Restore caches
10041006
uses: ./.github/actions/restore-cache
10051007
with:
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"imports": {
3+
"@sentry/deno": "npm:@sentry/deno",
4+
"redis": "npm:redis@^5.12.0"
5+
},
6+
"nodeModulesDir": "manual"
7+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
services:
2+
redis:
3+
image: redis:8
4+
restart: always
5+
container_name: e2e-tests-deno-redis
6+
ports:
7+
- '6379:6379'
8+
healthcheck:
9+
test: ['CMD', 'redis-cli', 'ping']
10+
interval: 1s
11+
timeout: 3s
12+
retries: 30
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { execSync } from 'child_process';
2+
import { dirname } from 'path';
3+
import { fileURLToPath } from 'url';
4+
5+
const __dirname = dirname(fileURLToPath(import.meta.url));
6+
7+
export default async function globalSetup() {
8+
// Start Redis via Docker Compose. `--wait` blocks until the healthcheck
9+
// in docker-compose.yml passes, so the Deno app can connect immediately.
10+
execSync('docker compose up -d --wait', {
11+
cwd: __dirname,
12+
stdio: 'inherit',
13+
});
14+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { execSync } from 'child_process';
2+
import { dirname } from 'path';
3+
import { fileURLToPath } from 'url';
4+
5+
const __dirname = dirname(fileURLToPath(import.meta.url));
6+
7+
export default async function globalTeardown() {
8+
execSync('docker compose down --volumes', {
9+
cwd: __dirname,
10+
stdio: 'inherit',
11+
});
12+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "deno-redis",
3+
"version": "1.0.0",
4+
"private": true,
5+
"scripts": {
6+
"start": "docker compose up -d --wait && deno run --allow-net --allow-env --allow-read --allow-sys --allow-write src/app.ts",
7+
"test": "playwright test",
8+
"clean": "npx rimraf node_modules pnpm-lock.yaml",
9+
"test:build": "pnpm install",
10+
"test:assert": "pnpm test"
11+
},
12+
"dependencies": {
13+
"@sentry/deno": "file:../../packed/sentry-deno-packed.tgz",
14+
"redis": "^5.12.0"
15+
},
16+
"devDependencies": {
17+
"@playwright/test": "~1.56.0",
18+
"@sentry-internal/test-utils": "link:../../../test-utils"
19+
},
20+
"volta": {
21+
"extends": "../../package.json"
22+
}
23+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
2+
3+
const config = getPlaywrightConfig({
4+
startCommand: `pnpm start`,
5+
port: 3030,
6+
});
7+
8+
export default {
9+
...config,
10+
globalSetup: './global-setup.mjs',
11+
globalTeardown: './global-teardown.mjs',
12+
};
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import * as Sentry from '@sentry/deno';
2+
import { createClient } from 'redis';
3+
4+
Sentry.init({
5+
environment: 'qa',
6+
dsn: Deno.env.get('E2E_TEST_DSN'),
7+
debug: !!Deno.env.get('DEBUG'),
8+
tunnel: 'http://localhost:3031/',
9+
tracesSampleRate: 1,
10+
});
11+
12+
// One shared client per process. node-redis publishes to the
13+
// `node-redis:command` / `:batch` / `:connect` diagnostics channels for every
14+
// operation on this client; denoRedisIntegration is already subscribed to
15+
// those.
16+
const redis = createClient({
17+
url: Deno.env.get('REDIS_URL') ?? 'redis://127.0.0.1:6379',
18+
});
19+
function onRedisError(err: unknown) {
20+
// eslint-disable-next-line no-console
21+
console.error('redis client error', err);
22+
}
23+
redis.on('error', onRedisError);
24+
await redis.connect();
25+
26+
const port = 3030;
27+
28+
Deno.serve({ port, hostname: '0.0.0.0' }, async (req: Request) => {
29+
const url = new URL(req.url);
30+
31+
// GET — exercises the command channel, success path.
32+
if (url.pathname === '/redis-get') {
33+
const key = url.searchParams.get('key') ?? 'cache:key';
34+
const value = await redis.get(key);
35+
return Response.json({ key, value });
36+
}
37+
38+
// SET then GET — exercises two commands inside a single transaction so we
39+
// can assert the parent has two db.redis children.
40+
if (url.pathname === '/redis-set-get') {
41+
const key = url.searchParams.get('key') ?? 'cache:key';
42+
const value = url.searchParams.get('value') ?? 'hello';
43+
await redis.set(key, value);
44+
const echoed = await redis.get(key);
45+
return Response.json({ key, value: echoed });
46+
}
47+
48+
// MULTI — exercises the batch channel.
49+
if (url.pathname === '/redis-multi') {
50+
const result = await redis.multi().set('multi:a', '1').set('multi:b', '2').get('multi:a').exec();
51+
return Response.json({ result });
52+
}
53+
54+
if (url.pathname === '/redis-disconnect') {
55+
redis.off('error', onRedisError);
56+
redis.close();
57+
return new Response('ok');
58+
}
59+
60+
return new Response('Not found', { status: 404 });
61+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { startEventProxyServer } from '@sentry-internal/test-utils';
2+
3+
startEventProxyServer({
4+
port: 3031,
5+
proxyServerName: 'deno-redis',
6+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForTransaction } from '@sentry-internal/test-utils';
3+
4+
test('GET command emits an http.server transaction containing a db.redis child span', async ({ baseURL }) => {
5+
// Each incoming request gets a Sentry http.server transaction (via the
6+
// default denoServeIntegration); the redis command runs inside it, so the
7+
// child span attaches to that transaction.
8+
const transactionPromise = waitForTransaction('deno-redis', event => {
9+
return (
10+
event?.contexts?.trace?.op === 'http.server' &&
11+
(event.request?.url ?? '').includes('/redis-get') &&
12+
(event.spans?.some(span => span.op === 'db.redis') ?? false)
13+
);
14+
});
15+
16+
const res = await fetch(`${baseURL}/redis-get?key=cache:user:42`);
17+
expect(res.status).toBe(200);
18+
await res.json();
19+
20+
const transaction = await transactionPromise;
21+
const redisSpan = transaction.spans!.find(span => span.op === 'db.redis');
22+
expect(redisSpan).toBeDefined();
23+
expect(redisSpan!.description).toBe('redis-GET');
24+
expect(redisSpan!.data?.['db.system']).toBe('redis');
25+
// Statement omits the value; for GET the only allowed arg is the key.
26+
expect(redisSpan!.data?.['db.statement']).toBe('GET cache:user:42');
27+
expect(redisSpan!.data?.['net.peer.port']).toBe(6379);
28+
});
29+
30+
test('SET then GET emit two db.redis child spans on the same transaction', async ({ baseURL }) => {
31+
const transactionPromise = waitForTransaction('deno-redis', event => {
32+
return (
33+
event?.contexts?.trace?.op === 'http.server' &&
34+
(event.request?.url ?? '').includes('/redis-set-get') &&
35+
(event.spans?.filter(span => span.op === 'db.redis').length ?? 0) >= 2
36+
);
37+
});
38+
39+
const res = await fetch(`${baseURL}/redis-set-get?key=cache:greeting&value=hello`);
40+
expect(res.status).toBe(200);
41+
await res.json();
42+
43+
const transaction = await transactionPromise;
44+
const redisSpans = transaction.spans!.filter(span => span.op === 'db.redis');
45+
expect(redisSpans.length).toBeGreaterThanOrEqual(2);
46+
const ops = redisSpans.map(s => s.description);
47+
expect(ops).toContain('redis-SET');
48+
expect(ops).toContain('redis-GET');
49+
});
50+
51+
test('MULTI batch emits a PIPELINE/MULTI batch span', async ({ baseURL }) => {
52+
const transactionPromise = waitForTransaction('deno-redis', event => {
53+
return (
54+
event?.contexts?.trace?.op === 'http.server' &&
55+
(event.request?.url ?? '').includes('/redis-multi') &&
56+
(event.spans?.some(span => span.description === 'MULTI' || span.description === 'PIPELINE') ?? false)
57+
);
58+
});
59+
60+
const res = await fetch(`${baseURL}/redis-multi`);
61+
expect(res.status).toBe(200);
62+
await res.json();
63+
64+
const transaction = await transactionPromise;
65+
const batchSpan = transaction.spans!.find(span => span.description === 'MULTI' || span.description === 'PIPELINE');
66+
expect(batchSpan).toBeDefined();
67+
expect(batchSpan!.op).toBe('db.redis');
68+
expect(batchSpan!.data?.['db.system']).toBe('redis');
69+
});
70+
71+
test('shut down redis client', async ({ baseURL }) => {
72+
const res = await fetch(`${baseURL}/redis-disconnect`);
73+
expect(await res.text()).toBe('ok');
74+
});

0 commit comments

Comments
 (0)